SWRのリクエストをMSWでインターセプトする際はキャッシュに気をつけよう!
はじめに
SWR
のリクエストをMSW
でインターセプトさせた際のつまづいた点を共有したいと思います。
技術スタック
個人的な話ですが、最近jest
からvitest
に乗り換えました。
vitest
早くていいですね。
パッケージ名 | バージョン |
---|---|
vitest | 2.1.5 |
swr | 2.2.5 |
msw | 2.6.6 |
テスト内容
Login
コンポーネントは、SWR
を使用して認証チェック用の API を呼び出す実装になっています。
内部では、ログイン状態に応じて挙動を切り替えており、ログイン中の場合は詳細ページ(/detail
)へ遷移し、ログインしていない場合はログインページ(/login
)に留まる動きとなっています。
この認証チェックのロジックをテストするため、MSW
でリクエストをインターセプトし、任意のレスポンスを返しています。
実装例
mocks/server
import { HttpResponse, http } from 'msw';
import { setupServer } from 'msw/node';
const AuthHandlers = [
// SWR が送信するリクエストをインターセプトし、任意のレスポンスを返す
http.get(`${BASE_URL}auth/`, () => {
return HttpResponse.json({
role: 'supervisor',
name: 'テスト 太郎',
});
}),
];
export const server = setupServer(...AuthHandlers);
テストコード
import { render, waitFor } from '@testing-library/react';
import mockRouter from 'next-router-mock';
import { server } from '../mocks/server';
import { Login } from '@/pages/Login';
describe('Auth hooks のテスト', () => {
afterEach(() => {
vi.clearAllMocks();
});
test('ログイン中のユーザーは詳細画面へ遷移する', async () => {
render(<Login />);
await waitFor(() => {
expect(mockRouter.asPath).toBe('/detail');
});
});
test('ログインしていないユーザーはログイン画面に留まる', async () => {
// server.use を使ってレスポンスを一時的に上書き
server.use(
http.get(
`${BASE_URL}auth/`,
() => {
return HttpResponse.json({
role: '',
name: '',
});
},
{ once: true }, // 一度だけこのレスポンスを使用
),
);
render(<Login />);
await waitFor(() => {
expect(mockRouter.asPath).toBe('/login');
});
});
});
つまづいたポイント
server.use
を利用してハンドラーを一時的に上書きしようとしましたが、上手くいきませんでした。
意図したレスポンスが適用されず、初期設定のレスポンス(role: 'supervisor', name: 'テスト 太郎'
)が返ってきました。
server.use(
http.get(
`${BASE_URL}auth/`,
() => {
return HttpResponse.json({
role: '',
name: '',
});
},
{ once: true },
),
);
render(<Login />);
// 上書きが失敗しているため、テスト結果も意図通りにならない
原因を調査したところ、SWR
のキャッシュが影響していると判明しました。
公式ドキュメントによると、各テストの前後でキャッシュをクリアする必要があると記載されていました。
Caching mechanism of some request clients may produce stale responses in your tests. Make sure you clear the cache before/after each test suite for your tests to remain predictable.
これを受け、公式の推奨に従ってキャッシュをクリアしようとしましたが、SWR
のアップデートにより cache
は削除されていました。
import { cache } from 'swr';
// 'cache' という名前のエクスポートされたメンバーが '"swr"' に含まれていません。候補: 'Cache'
そこで、SWR の公式ドキュメントを確認したところ、SWRConfig
を活用することでキャッシュをリセットする方法が紹介されていました。
修正版テストコード
公式ドキュメントに従い、SWRConfig
の provider
プロパティを利用して新しいキャッシュを生成します。
これにより、SWR
のキャッシュがリセットされ、意図したレスポンスを返せるようになります。
test('ユーザーがログイン中ではない場合、詳細画面には遷移しない', async () => {
// 一時的にレスポンスを上書き
server.use(
http.get(
`${BASE_URL}auth/`,
() => {
return HttpResponse.json({
role: '',
name: '',
});
},
{ once: true }, // 一度だけこのレスポンスを使用
),
);
// SWRConfig を使用してキャッシュをリセット
render(
<SWRConfig value={{ provider: () => new Map() }}>
<Login />
</SWRConfig>
);
await waitFor(() => {
expect(mockRouter.asPath).toBe('/login');
});
});
なお、hookのテストでは以下のように対応します。
SWRConfig
をラップ用のコンポーネントとして定義し、renderHook
に渡すことで、テストごとにキャッシュをリセットできます。
test('ユーザーがログイン中ではない場合、詳細画面には遷移しない', async () => {
server.use(
http.get(
`${BASE_URL}auth/`,
() => {
return HttpResponse.json({
role: '',
name: '',
});
},
{ once: true },
),
);
const wrapper = ({ children }: { children: ReactNode }) => (
<SWRConfig value={{ provider: () => new Map() }}>
{children}
</SWRConfig>
);
renderHook(() => useAuth(), { wrapper });
await waitFor(() => {
expect(mockRouter.asPath).toBe('/login');
});
});
終わりに
キャッシュってtestにも影響するんですね、難しい。。
Discussion