💰

SWRのリクエストをMSWでインターセプトする際はキャッシュに気をつけよう!

2024/12/03に公開

はじめに

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.

https://mswjs.io/docs/faq/#why-do-i-get-stale-responses-with-react-queryswrapolloetc

これを受け、公式の推奨に従ってキャッシュをクリアしようとしましたが、SWR のアップデートにより cache は削除されていました。

import { cache } from 'swr';
// 'cache' という名前のエクスポートされたメンバーが '"swr"' に含まれていません。候補: 'Cache'

そこで、SWR の公式ドキュメントを確認したところ、SWRConfig を活用することでキャッシュをリセットする方法が紹介されていました。
https://swr.vercel.app/ja/docs/advanced/cache

修正版テストコード

公式ドキュメントに従い、SWRConfigprovider プロパティを利用して新しいキャッシュを生成します。
これにより、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