React Testing Libraryの勉強メモ
React Testing Libraryについて勉強したので備忘録です。 ソースコードはこちら
テストの種類
Unit Test, Integration Testの手法 → Jest + React Testing Library(EnzymeよりHooksとの相性がいい)
Jest
- npm test
- test case
- expect
React Testing Library
- render
- userEvent
- custom hook
CLI
Test Suites: テストが書かれたファイルの数
Tests: 書かれたテストの数
テストの名前を表示させるには
"test": "react-scripts test --env=jsdom --verbose"
UserEvent Test
it("Should trigger output function", async () => { const outputConsole = jest.fn(); render(<RenderInput outputConsole={outputConsole} />); const inputValue = screen.getByPlaceholderText("Enter"); await userEvent.type(inputValue, "test"); await userEvent.click(screen.getByRole("button")); expect(outputConsole).toHaveBeenCalledTimes(1); });
jest.fn()
でダミーのprops関数を定義し、その関数が何回呼ばれたかを評価するuseEvent
はPromiseを返す(React18〜)getByRole
で使うroleは明示的に定義していなければhtmlタグとの相関はこのようになっている
API Mock Test
import { rest } from "msw"; import { setupServer } from "msw/node"; import MockServer from "./MockServer"; // 代用されるAPIの定義 const server = setupServer( rest.get("https://jsonplaceholder.typicode.com/users/1", (req, res, ctx) => { return res(ctx.status(200), ctx.json({ username: "Bred dummy " })); }) ); // テストファイルの最初に1回実行される beforeAll(() => server.listen()); // テストケースが終わる度に実行される afterEach(() => { server.resetHandlers(); }); // テストファイルの最後に1回実行される afterAll(() => { server.close(); }); describe("Mocking API", () => { it("[Fetch Success] データが正しくフェッチされてボタンがdisabledになる", async () => { render(<MockServer />); await userEvent.click(screen.getByRole("button")); expect(await screen.findByRole("heading")).toHaveTextContent("Bred dummy"); expect(screen.getByRole("button")).toHaveAttribute("disabled"); }); it("[Fetch failure] エラーメッセージが表示されボタンが有効化される", async () => { // レスポンスを書き換える、このitの中でのみ有効 server.use( rest.get( "https://jsonplaceholder.typicode.com/users/1", (req, res, ctx) => { return res(ctx.status(404)); } ) ); render(<MockServer />); await userEvent.click(screen.getByRole("button")); expect(await screen.findByTestId("error")).toHaveTextContent( "Fetching Failed!" ); expect(screen.queryByRole("heading")).toBeNull(); expect(screen.getByRole("button")).not.toHaveAttribute("disabled"); }); });
- getBy, queryBy, findBy
- 通常はgetBy
- getByは対象がないとエラーになるため、対象が 存在しない ことを評価するためには queryBy
- 非同期で対象が見つかるまで待機する場合はPromiseを返す findBy
Reducer Test
describe("increment action", () => { let initialState = { mode: 0, value: 1, }; it("Should increment by 1 with mode 0", () => { const action = { type: increment.type }; const state = reducer(initialState, action); expect(state.value).toEqual(2); }); it("Should increment by 100 with mode 1", () => { initialState = { mode: 1, value: 1, }; const action = { type: increment.type }; const state = reducer(initialState, action); expect(state.value).toEqual(101); }); });
Extra Reducer Test
const initialState = { mode: 0, value: 0, }; it("Should output 100 + payload when fulfilled", () => { const action = { type: fetchDummy.fulfilled.type, payload: 5 }; const state = reducer(initialState, action); expect(state.value).toEqual(105); });
Integration Test
let store; beforeEach(() => { store = configureStore({ reducer: { customCounter: customCounterSlice, }, }); }); it("Should display value with incrementByAmount", async () => { render( <Provider store={store}> <Redux /> </Provider> ); await userEvent.type(screen.getByPlaceholderText("Enter"), "30"); await userEvent.click(screen.getByText("Increment By Amount")); expect(screen.getByTestId("count-value")).toHaveTextContent(30); });
CustomHook Test
it("Should increment by 1", () => { const { result } = renderHook(() => useCounter(3)); expect(result.current.count).toBe(3); act(() => { result.current.increment(); }); expect(result.current.count).toBe(4); });
- renderHook はコンポーネント外でhookを実行できるようにするための関数
- 内部でstateに変更を加える等のふるまいがある場合は act内で実行する
ふむふむ