かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

Jest × React Testing Library テスト内のイベントが state の更新を待ってくれないにハマる

Jest × React Testing Library の勉強をしています。
今回は state が更新されるかをテストしていて、テスト内の useEvent.click 直後の expect が state が更新される前の値となってしまいテストが落ちてしまったメモです

環境
  • jest 26.6.3
  • @testing-library/dom 7.29.2
  • @testing-library/jest-dom 5.17.0
  • @testing-library/react 14.0.0
  • @testing-library/user-event 14.4.3
  • React 18.2.0
  • TypeScript 5.1.6

React Component

構成としてはシンプルに Context に持たせた state (toggleFlg) がコンポーネントのボタンを押すと変更されるというもの

/src/StateProvider.tsx

interface IContext {
  toggleFlg: boolean;
  setToggleFlg: React.Dispatch<React.SetStateAction<boolean>>;
}

type StateProviderProps = {
  children: ReactNode;
};
export const StateProvider: FC<StateProviderProps> = ({ children }) => {
  const [toggleFlg, setToggleFlg] = useState<boolean>(false);
  return (
    <StateContext.Provider value={{ toggleFlg, setToggleFlg }}>
      {children}
    </StateContext.Provider>
  );
};

export const useStateContext = () => useContext(StateContext);

/src/ChangeToggleFlgButton.tsx

import { useStateContext } from '@/StateProvider';
export const ChangeToggleFlgButton: FC = () => {
  const { setToggleFlg } = useStateContext();
  return (
    <button type="button" onClick={() => {setToggleFlg((flg) => !flg)}}>
      change
    </button>
  );
};

/src/Content.tsx

import { useStateContext } from '@/StateProvider';
export const Content:FC = () => {
  const { toggleFlg } = useStateContext();
  return (
    <div>
      <p>Context</p>
      <code data-testid='toggleFlg'>{toggleFlg ? 'true' : 'false'}</code>
    </div>
  );
};

useEvent.click 直後の expect 内で state がまだ更新されてない状態になる

ChangeToggleFlgButton をクリックしたら state が更新され ContexttoggleFlg の値が変更されることをテストしたい

/src/__test__/Context.test.tsx;

import '@testing-library/jest-dom/extend-expect';
import useEvent from '@testing-library/user-event';
import { render, screen } from '@testing-library/react';
import { Content } from '@/Content';
import { ChangeToggleFlgButton } from '@/ChangeToggleFlgButton';
import { StateProvider } from '@/StateProvider';

describe('StateProvider を使った state の変更テスト', () => {
  it('ChangeToggleFlgButton をクリックしたら toggleFlg の値が変更されること', () => {
    render(
      <StateProvider>
        <ChangeToggleFlgButton />
        <Context />
      </StateProvider>,
    );
    // render 直後 toggleFlg は false
    expect(screen.getByTestId('toggleFlg').textContent).toBe('false');
    // ChangeToggleFlgButton をクリック
    useEvent.click(screen.getByRole('button'));
    // クリック後 toggleFlg は true
    expect(screen.getByTestId('toggleFlg').textContent).toBe('true');
  });
});

👇 テストが落ちる

FAIL  src/__tests__/Context.test.tsx
  StateProvider を使った state の変更テスト
    ✕ ChangeToggleFlgButton をクリックしたら toggleFlg の値が変更されること (23 ms)
  expect(received).toBe(expected) // Object.is equality
    Expected: "true"
    Received: "false"
    > 21 |     expect(screen.getByTestId('toggleFlg').textContent).toBe('true');

toggleFlg の値が "true" を期待しているが "false" なのでテストが落ちる
つまり、クリックが実行されて state が更新される前に DOM から値を取っている状態っぽい

解決方法: waitForuseEvent.click を囲って state が更新されるのを待たせる

/src/__test__/Context.test.tsx;

import '@testing-library/jest-dom/extend-expect';
import useEvent from '@testing-library/user-event';
import { render, screen, waitFor } from '@testing-library/react';
// 略
describe('StateProvider を使った state の変更テスト', async () => {
  it('ChangeToggleFlgButton をクリックしたら toggleFlg の値が変更されること', () => {
    render(
      <StateProvider>
        <ChangeToggleFlgButton />
        <Context />
      </StateProvider>,
    );
    // render 直後 toggleFlg は false
    expect(screen.getByTestId('toggleFlg').textContent).toBe('false');
    // waitFor を使って state が更新されるのを待つ
    await waitFor(async () => {
      // ChangeToggleFlgButton をクリック
      await useEvent.click(screen.getByRole('button'));
    });
    // クリック後 toggleFlg は true
    expect(screen.getByTestId('toggleFlg').textContent).toBe('true');
  });
});

👇 テスト

 PASS  src/__tests__/Context.test.tsx
  StateProvider を使った state の変更テスト
    ✓ ChangeToggleFlgButton をクリックしたら toggleFlg の値が変更されること (46 ms)

 

バージョンが新しいの使えばいいとか GitHub でも色々議論されているようだが、テスト周りの知見がなさすぎて何が正しいのかイマイチ把握できていない。
今はとりあえず useEvent 後の expect 内で state の更新が待たれない場合は waitFor を使ってみると良いのかな。くらいに思っておきます。


[参考]