React Testing Libraryとは?
React Testing Libraryは、Reactコンポーネントをテストするための非常に便利なソリューションです。軽量で便利なAPIを提供してくれていて、Reactサイトのドキュメントを開くと、テストの項目中で推奨ツールとして使われていることがわかります。
基础知识
it
/ test
: テスト自体を記述するために使用され、2つの引数が含まれています。1つ目はそのテストの説明であり、2つ目はそのテストを実行する関数です。
expect
: テストに合格する必要がある条件を示し、自体のパラメータをmatcher
と比較します。
matcher
: 目的の条件に到達することが期待される関数です。
render
: 与えられたコンポーネントをレンダリングするために使用される関数です。
例えば、it
を使ってテストを記述し、render
関数を使ってAppコンポーネントをレンダリングし、`asFragment(<App />)
の結果がtoMatchSnapshot()
というmatcher
に合うことを期待しています。
import React from "react";
import { render } from "@testing-library/react";
import App from "./App";
it("snapshot test", () => {
const { asFragment } = render(<App />);
expect(asFragment(<App />)).toMatchSnapshot();
});
一般的な使用方法の例
1. テストスナップショットの作成
スナップショットでは、指定されたコンポーネントのスナップショットを保存することができる。コンポーネントの更新やリファクタリングを行い、変更点を取得したり比較したりしたいときに非常に便利です。
現在、App.jsのスナップショットテストをしてみましょう。
render
を使ってAppコンポーネントをレンダリングし、その関数からasFragment
を返すことができます。最後に、コンポーネントのフラグメントがスナップショットと一致していることを確認します。
import React from "react";
import { render, cleanup } from "@testing-library/react";
import App from "./App";
// メモリリークを避けるために、各テスト後にすべてのコンテンツを消去する
afterEach(cleanup);
it("should take a snapshot", () => {
// コンポーネントをレンダリングする
const { asFragment } = render(<App />);
expect(asFragment(<App />)).toMatchSnapshot();
});
2.DOM要素のテスト
まず、テストしたいコンポーネントを作ります。
// TestElements.jsx
import React from "react";
const TestElements = () => {
const [counter, setCounter] = React.useState(0);
return (
<>
<h1 data-testid="counter">{counter}</h1>
<button data-testid="button-up" onClick={() => setCounter(counter + 1)}>Up</button>
<button
disabled
data-testid="button-down"
onClick={() => setCounter(counter - 1)}
>
Down
</button>
</>
);
};
export default TestElements;
気をつけなければならないのは、data-testidです。 これはテストコードからdom要素を取得するために使用されます。
では、テストを書いてみましょう。
1.Counterが0
に等しいかどうかをテストします。
// TestElements1.test.jsx
import React from "react";
import { render, cleanup } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";
import TestElements from "./TestElements";
afterEach(cleanup);
it("Counter should equal to 0", () => {
const { getByTestId } = render(<TestElements />);
expect(getByTestId("counter")).toHaveTextContent(0);
});
2.ボタンが無効になっているか、有効になっているかをテストします。
// TestElements2.test.jsx
import React from "react";
import { render, cleanup } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";
import TestElements from "./TestElements";
afterEach(cleanup);
it("Button should be enabled", () => {
const { getByTestId } = render(<TestElements />);
expect(getByTestId("button-up")).not.toHaveAttribute("disabled");
});
it("Button should be disabled", () => {
const { getByTestId } = render(<TestElements />);
expect(getByTestId("button-down")).toBeDisabled();
});
3.イベントのテスト
まず、テストしたいコンポーネントを作ります。
// TestEvents.jsx
import React from "react";
const TestEvents = () => {
const [counter, setCounter] = React.useState(0);
return (
<>
<h1 data-testid="counter">{counter}</h1>
<button data-testid="button-up" onClick={() => setCounter(counter + 1)}>
Up
</button>
<button data-testid="button-down" onClick={() => setCounter(counter - 1)}>
Down
</button>
</>
);
};
export default TestEvents;
そして、テストを書いてみましょう。
1.ボタンをクリックするだけでcounterが正しく+1、-1されることをテストします。
ここでfireEvent.click()でクリックのイベントをトリガーします。fireEvent
には、イベントをテストするためのいくつかの関数があります。
// TestEvents.test.jsx
import React from "react";
import { render, cleanup, fireEvent } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";
import TestEvents from "./TestEvents";
afterEach(cleanup);
it("increments counter", () => {
const { getByTestId } = render(<TestEvents />);
// fireEvent.click()でクリックのイベントをトリガーする
fireEvent.click(getByTestId("button-up"));
expect(getByTestId("counter")).toHaveTextContent("1");
});
it("decrements counter", () => {
const { getByTestId } = render(<TestEvents />);
// fireEvent.click()でクリックのイベントをトリガーする
fireEvent.click(getByTestId("button-down"));
expect(getByTestId("counter")).toHaveTextContent("-1");
});
4.非同期操作のテスト
まず、テストしたいコンポーネントを作ります。
ここでsetTimeout()
を使用して、非同期操作をシミュレートします。
// TestAsync.jsx
import React from "react";
const TestAsync = () => {
const [counter, setCounter] = React.useState(0);
const delayCount = () =>
setTimeout(() => {
setCounter(counter + 1);
}, 500);
return (
<>
<h1 data-testid="counter">{counter}</h1>
<button data-testid="button-up" onClick={delayCount}>
Up
</button>
<button data-testid="button-down" onClick={() => setCounter(counter - 1)}>
Down
</button>
</>
);
};
export default TestAsync;
そして、テストを書いてみましょう。
まず、非同期操作を処理するためにasync/await
を使用しなければなりません。 次に、新しい関数getByText()
を使用します。これはgetByTestId()
に似ていますが、以前使用していたtest-id
ではなく、テキストの内容によってdom要素を取得できます。
// TestAsync.test.jsx
import React from "react";
import {
render,
cleanup,
fireEvent,
waitForElement,
} from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";
import TestAsync from "./TestAsync";
afterEach(cleanup);
it("increments counter after 0.5s", async () => {
const { getByTestId, getByText } = render(<TestAsync />);
fireEvent.click(getByTestId("button-up"));
const counter = await waitForElement(() => getByText("1"));
expect(counter).toHaveTextContent("1");
});
5.React Reduxのテスト
まず、テストしたいコンポーネントを作ります。
// TestRedux.jsx
import React from "react";
import { connect } from "react-redux";
const TestRedux = ({ counter, dispatch }) => {
const increment = () => dispatch({ type: "INCREMENT" });
const decrement = () => dispatch({ type: "DECREMENT" });
return (
<>
<h1 data-testid="counter">{counter}</h1>
<button data-testid="button-up" onClick={increment}>
Up
</button>
<button data-testid="button-down" onClick={decrement}>
Down
</button>
</>
);
};
export default connect((state) => ({ counter: state.count }))(TestRedux);
// store/reducer.js
export const initialState = {
count: 0,
};
export function reducer(state = initialState, action) {
switch (action.type) {
case "INCREMENT":
return {
count: state.count + 1,
};
case "DECREMENT":
return {
count: state.count - 1,
};
default:
return state;
}
}
そして、テストを書いてみましょう。
1.初期状態が0に等しいかどうかをテストする
ここでは、コンポーネントをレンダリングするための独自のヘルパー関数renderWithRedux()
を作成しました。renderWithRedux()
は、レンダリングするコンポーネント、initialState
、store
を引数として受け取ります。store
がない場合は新しいstore
を作成し、initialState
またはstore
を受信しなかった場合は空のオブジェクトを返します。次にrender()
を使用してコンポーネントをレンダリングし、store
をProvider
に渡します。
// TestRedux1.test.jsx
import React from "react";
import { createStore } from "redux";
import { Provider } from "react-redux";
import { render, cleanup, fireEvent } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";
import { initialState, reducer } from "../store/reducer";
import TestRedux from "./TestRedux";
const renderWithRedux = (
component,
{ initialState, store = createStore(reducer, initialState) } = {}
) => {
return {
...render(<Provider store={store}>{component}</Provider>),
store,
};
};
afterEach(cleanup);
it("checks initial state is equal to 0", () => {
const { getByTestId } = renderWithRedux(<TestRedux />);
expect(getByTestId("counter")).toHaveTextContent("0");
});
2.Counterの+1と-1が正しいかどうかのテスト
+1と-1のイベントをテストするために、2番目のパラメータとしてinitialState
をrenderWithRedux()
に渡します。
// TestRedux2.test.jsx
import React from "react";
import { createStore } from "redux";
import { Provider } from "react-redux";
import { render, cleanup, fireEvent } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";
import { initialState, reducer } from "../store/reducer";
import TestRedux from "./TestRedux";
const renderWithRedux = (
component,
{ initialState, store = createStore(reducer, initialState) } = {}
) => {
return {
...render(<Provider store={store}>{component}</Provider>),
store,
};
};
afterEach(cleanup);
it("increments the counter through redux", () => {
const { getByTestId } = renderWithRedux(<TestRedux />, {
initialState: { count: 5 },
});
fireEvent.click(getByTestId("button-up"));
expect(getByTestId("counter")).toHaveTextContent("6");
});
it("decrements the counter through redux", () => {
const { getByTestId } = renderWithRedux(<TestRedux />, {
initialState: { count: 100 },
});
fireEvent.click(getByTestId("button-down"));
expect(getByTestId("counter")).toHaveTextContent("99");
});
6.React Routerのテスト
まず、テストしたいコンポーネントを作ります。
// TestRouter.jsx
import React from "react";
import { Link, Route, Switch, useParams } from "react-router-dom";
const About = () => <h1>About page</h1>;
const Home = () => <h1>Home page</h1>;
const Contact = () => {
const { name } = useParams();
return <h1 data-testid="contact-name">{name}</h1>;
};
const TestRouter = () => {
const name = "TEST";
return (
<>
<nav data-testid="navbar">
<Link data-testid="home-link" to="/">
Home
</Link>
<Link data-testid="about-link" to="/about">
About
</Link>
<Link data-testid="contact-link" to={`/contact/${name}`}>
Contact
</Link>
</nav>
<Switch>
<Route exact path="/" component={Home} />
<Route path="/about" component={About} />
<Route path="/about:name" component={Contact} />
</Switch>
</>
);
};
export default TestRouter;
そして、テストを書いてみましょう。
1.ルートの切り替え時に、正しくレンダリングされるかどうかをテストする
React Router
をテストするには、まずhistory
が必要なので、createMemoryHistory()
を使ってhistory
を作成します。次に、ヘルパー関数renderWithRouter()
を使用してコンポーネントをレンダリングし、history
をRouterコンポーネントに渡します。これで、最初に読み込んだページがホームページであるかどうかをテストし、期待されるLinkコンポーネントをナビゲーションバーに表示することができるようになりました。
// TestRouter1.test.jsx
import React from "react";
import { Router } from "react-router-dom";
import { render, fireEvent } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";
import { createMemoryHistory } from "history";
import TestRouter from "./TestRouter";
// Helper function
const renderWithRouter = (component) => {
const history = createMemoryHistory();
return {
...render(<Router history={history}>{component}</Router>),
};
};
it("should render the home page", () => {
const { container, getByTestId } = renderWithRouter(<TestRouter />);
const navbar = getByTestId("navbar");
const link = getByTestId("home-link");
expect(container.innerHTML).toMatch("Home page");
expect(navbar).toContainElement(link);
});
2.Linkをクリックすると、別のページに移動するかどうかをテストする
Linkが動作しているかどうかを確認するには、Linkのクリックイベントを発生させる必要があります。
// TestRouter2.test.jsx
import React from "react";
import { Router } from "react-router-dom";
import { render, fireEvent } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";
import { createMemoryHistory } from "history";
import TestRouter from "./TestRouter";
// Helper function
const renderWithRouter = (component) => {
const history = createMemoryHistory();
return {
...render(<Router history={history}>{component}</Router>),
};
};
it("should navigate to the about page", () => {
const { container, getByTestId } = renderWithRouter(<TestRouter />);
fireEvent.click(getByTestId("about-link"));
expect(container.innerHTML).toMatch("About page");
});
it("should navigate to the contact page with the params", () => {
const { container, getByTestId } = renderWithRouter(<TestRouter />);
fireEvent.click(getByTestId("contact-link"));
expect(container.innerHTML).toMatch("TEST");
});
7.HTTP Requestのテスト
まず、テストしたいコンポーネントを作ります。
簡単なコンポーネントにリクエストボタンを実装します。 そして、データが利用できない場合は、メッセージ(Loading...)
を表示します。
// TestAxios.jsx
import React from "react";
import axios from "axios";
const TestAxios = ({ url }) => {
const [data, setData] = React.useState();
const fetchData = async () => {
const response = await axios.get(url);
setData(response.data.greeting);
};
return (
<>
<button onClick={fetchData} data-testid="fetch-data">
Load Data
</button>
{data ? (
<div data-testid="show-data">{data}</div>
) : (
<h1 data-testid="loading">Loading...</h1>
)}
</>
);
};
export default TestAxios;
そして、テストを書いてみましょう。
1.データが正しく取得され、表示されていることをテストする
HTTPのリクエストを処理するためには、 jest.mock('axios')
でaxios
リクエストをシミュレートする必要があります。あとはaxiosMock
のget()
関数とJestの組み込み関数mockResolvedValueOnce()
を使って、シミュレートされたデータを引数として渡します。2番目のテストでは、非同期リクエストを処理するためにasync/await
を使用しなければなリません。
// TestAxios.test.jsx
import React from "react";
import { render, waitForElement, fireEvent } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";
import axiosMock from "axios";
import TestAxios from "./TestAxios";
jest.mock("axios");
it("should display a loading text", () => {
const { getByTestId } = render(<TestAxios />);
expect(getByTestId("loading")).toHaveTextContent("Loading...");
});
it("should load and display the data", async () => {
const url = "/greeting";
const { getByTestId } = render(<TestAxios url={url} />);
axiosMock.get.mockResolvedValueOnce({
data: { greeting: "hello there" },
});
fireEvent.click(getByTestId("fetch-data"));
const greetingData = await waitForElement(() => getByTestId("show-data"));
expect(axiosMock.get).toHaveBeenCalledTimes(1);
expect(axiosMock.get).toHaveBeenCalledWith(url);
expect(greetingData).toHaveTextContent("hello there");
});
終わりに
ここまでで、簡単な7つのステップでほどんとReactアプリをテストできました。
React Testing Libraryを使えば、Reactアプリを完全にテストしやすくなり、テストを書くのが楽しくなり、もうコンポーネントを気にする必要がない生活を享受することができるようになります。