本記事の内容
React公式ドキュメント推奨のーJestとReact Testing LibraryでReduxアプリをテストする際の環境整備を説明する。
react-redux
、redux-toolkit
どちらの書き方でも同様に使える。
前提条件
React: "^18.2.0"
react-redux: "^8.0.5"
@reduxjs/toolkit: "^1.9.1"
@testing-library/jest-dom: "^5.16.5"
@testing-library/react: "^13.4.0"
@testing-library/user-event: "^14.4.3"
@types/jest: "^29.4.0"
環境整備
今回は、CRA(create-react-app)を使用する。デフォルトでは、Jest
やtesting-library
系がすでにインストールされている。(助かる)
// package.json
"dependencies": {
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2",
// ・・・省略
},
Reduxアプリをテストするために
さて、Reduxを含めたアプリをテストするために、React-Redux
がexportしている<Provider>
コンポーネントでテスト対象のコンポーネントツリーをラッピングする必要がある。
import { Provider } from "react-redux";
import { store } from './store';
import Counter from ".";
test("+ボタンを押すと数字が1増える", () => {
// レンダリング
render(
<Provider store={store}>
<Counter />
</Provider>
);
// アクション
// ・・・略
// アサート
// ・・・略
});
しかし、これをケースごとに書いていくと、記述が冗長になってしまう。
import { Provider } from "react-redux";
import { store } from './store';
import Counter from ".";
test("+ボタンを押すと数字が1プラスされる", () => {
// レンダリング
render(
<Provider store={store}>
<Counter />
</Provider>
);
// アクション
// ・・・略
// アサート
// ・・・略
});
test("-ボタンを押すと数字が1マイナスされる", () => {
// レンダリング
render(
<Provider store={store}>
<Counter />
</Provider>
);
// アクション
// ・・・略
// アサート
// ・・・略
});
この冗長な記述を簡略化するために、<Provider>
を共通化しようじゃないか!という素晴らしい?方法をReduxの公式ドキュメントで提案している。
詳細は、本家を覗いてみるとよい。ここで簡単に説明すると、ラッピングの<Provider>
コンポーネントを共通化して、レンダリング時にchildren
でテスト対象のコンポーネントを渡すことにしている。
// utils/test-utils.tsx
import React, { PropsWithChildren } from 'react'
import { render } from '@testing-library/react'
import type { RenderOptions } from '@testing-library/react'
import { configureStore } from '@reduxjs/toolkit'
import type { PreloadedState } from '@reduxjs/toolkit'
import { Provider } from 'react-redux'
import type { AppStore, RootState } from '../app/store'
// As a basic setup, import your same slice reducers
import counterReducer from '../features/counter/counterSlice'
// This type interface extends the default options for render from RTL, as well
// as allows the counter to specify other things such as initialState, store.
interface ExtendedRenderOptions extends Omit<RenderOptions, 'queries'> {
preloadedState?: PreloadedState<RootState>
store?: AppStore
}
export function renderWithProviders(
ui: React.ReactElement,
{
preloadedState = {},
// Automatically create a store instance if no store was passed in
store = configureStore({ reducer: { counter: counterReducer }, preloadedState }),
...renderOptions
}: ExtendedRenderOptions = {}
) {
function Wrapper({ children }: PropsWithChildren<{}>): JSX.Element {
return <Provider store={store}>{children}</Provider>
}
// Return an object with the store and all of RTL's query functions
return { store, ...render(ui, { wrapper: Wrapper, ...renderOptions }) }
}
また、preloadedState
で、store
の初期値をテストごとに設定できるので、大変便利ではないかなと感じている。
共通のutil
として、上記で共通化できたら、それぞれのテストファイルで下記のように記述する。
import { Provider } from "react-redux";
import { renderWithProviders } from '../utils/test-utils';
import { store } from './store';
import Counter from ".";
test("+ボタンを押すと数字が1プラスされる", () => {
// レンダリング
renderWithProviders(<Counter />);
// アクション
// ・・・略
// アサート
// ・・・略
});
test("-ボタンを押すと数字が1マイナスされる", () => {
// レンダリング
renderWithProviders(<Counter />);
// アクション
// ・・・略
// アサート
// ・・・略
});
共通化する前の5行から1行にまとめられて、ずいぶんスッキリしましたね。
ちなみに、preloadedState
でテストの初期値を設定するときは下記のように記述できる。
test("+ボタンを押すと数字が1プラスされる", () => {
const initialCount = 5;
// レンダリング
renderWithProviders(<Counter />, {
preloadedState: {
currentCount: initialCount
}
});
// アクション
// ・・・略
// アサート
// ・・・略
});
次回について
今回は、Redux
アプリの自動テスト環境構築をやってみた。
次回は、RTK Query
とmsw
使用した非同期通信をテストする場合の追加設定の記事を作成していく予定でござる。
最後までお読みいただきありがとうございました。
参考記事
Setting Up a Test Environment - redux公式ドキュメント
【React,Redux】JEST+testing-library+MSWで始めるテスト入門