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 が更新され Context
の toggleFlg
の値が変更されることをテストしたい
/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 から値を取っている状態っぽい
解決方法: waitFor
で useEvent.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
を使ってみると良いのかな。くらいに思っておきます。
[参考]