React Testing Libraryを使ってReactアプリをテストする方法

### [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/)とは？
&nbsp;&nbsp;&nbsp;&nbsp;React Testing Libraryは、Reactコンポーネントをテストするための非常に便利なソリューションです。軽量で便利なAPIを提供してくれていて、Reactサイトの[ドキュメント](https://ja.reactjs.org/docs/testing.html)を開くと、テストの項目中で推奨ツールとして使われていることがわかります。


### 基础知识
`it` / `test`: テスト自体を記述するために使用され、2つの引数が含まれています。1つ目はそのテストの説明であり、2つ目はそのテストを実行する関数です。
`expect`: テストに合格する必要がある条件を示し、自体のパラメータを`matcher`と比較します。
`matcher`: 目的の条件に到達することが期待される関数です。
`render`:　与えられたコンポーネントをレンダリングするために使用される関数です。

&nbsp;&nbsp;&nbsp;&nbsp;例えば、`it`を使ってテストを記述し、`render`関数を使ってAppコンポーネントをレンダリングし、｀`asFragment(<App />)`の結果が`toMatchSnapshot()`という`matcher`に合うことを期待しています。

```jsx
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. テストスナップショットの作成
&nbsp;&nbsp;&nbsp;&nbsp;スナップショットでは、指定されたコンポーネントのスナップショットを保存することができる。コンポーネントの更新やリファクタリングを行い、変更点を取得したり比較したりしたいときに非常に便利です。

&nbsp;&nbsp;&nbsp;&nbsp;現在、App.jsのスナップショットテストをしてみましょう。
&nbsp;&nbsp;&nbsp;&nbsp;`render`を使ってAppコンポーネントをレンダリングし、その関数から`asFragment`を返すことができます。最後に、コンポーネントのフラグメントがスナップショットと一致していることを確認します。

```jsx
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要素のテスト

&nbsp;&nbsp;&nbsp;&nbsp;まず、テストしたいコンポーネントを作ります。

```jsx
// 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;

```
&nbsp;&nbsp;&nbsp;&nbsp;気をつけなければならないのは、data-testidです。 これはテストコードからdom要素を取得するために使用されます。
&nbsp;&nbsp;&nbsp;&nbsp;では、テストを書いてみましょう。

&nbsp;&nbsp;&nbsp;&nbsp;1.Counterが`0`に等しいかどうかをテストします。

```jsx
// 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);
});
```
&nbsp;&nbsp;&nbsp;&nbsp;2.ボタンが無効になっているか、有効になっているかをテストします。

```jsx
// 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.イベントのテスト
&nbsp;&nbsp;&nbsp;&nbsp;まず、テストしたいコンポーネントを作ります。

```jsx
// 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;
```
&nbsp;&nbsp;&nbsp;&nbsp;そして、テストを書いてみましょう。

&nbsp;&nbsp;&nbsp;&nbsp;1.ボタンをクリックするだけでcounterが正しく+1、-1されることをテストします。
&nbsp;&nbsp;&nbsp;&nbsp;ここでfireEvent.click()でクリックのイベントをトリガーします。`fireEvent`には、イベントをテストするためのいくつかの関数があります。

```jsx
// 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.非同期操作のテスト
&nbsp;&nbsp;&nbsp;&nbsp;まず、テストしたいコンポーネントを作ります。
&nbsp;&nbsp;&nbsp;&nbsp;ここで`setTimeout()`を使用して、非同期操作をシミュレートします。

```jsx
// 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;
```
&nbsp;&nbsp;&nbsp;&nbsp;そして、テストを書いてみましょう。
&nbsp;&nbsp;&nbsp;&nbsp;まず、非同期操作を処理するために`async/await`を使用しなければなりません。 次に、新しい関数`getByText()`を使用します。これは`getByTestId()`に似ていますが、以前使用していた`test-id`ではなく、テキストの内容によってdom要素を取得できます。

```jsx
// 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のテスト
&nbsp;&nbsp;&nbsp;&nbsp;まず、テストしたいコンポーネントを作ります。

```jsx
// 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);
```
```jsx
// 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;
  }
}
```
&nbsp;&nbsp;&nbsp;&nbsp;そして、テストを書いてみましょう。

&nbsp;&nbsp;&nbsp;&nbsp;1.初期状態が0に等しいかどうかをテストする
&nbsp;&nbsp;&nbsp;&nbsp;ここでは、コンポーネントをレンダリングするための独自のヘルパー関数`renderWithRedux()`を作成しました。`renderWithRedux()`は、レンダリングするコンポーネント、`initialState`、`store`を引数として受け取ります。`store`がない場合は新しい`store`を作成し、`initialState`または`store`を受信しなかった場合は空のオブジェクトを返します。次に`render()`を使用してコンポーネントをレンダリングし、`store`を`Provider`に渡します。

```jsx
// 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");
});
```
&nbsp;&nbsp;&nbsp;&nbsp;2.Counterの+1と-1が正しいかどうかのテスト
&nbsp;&nbsp;&nbsp;&nbsp;+1と-1のイベントをテストするために、2番目のパラメータとして`initialState`を`renderWithRedux()`に渡します。

```jsx
// 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のテスト
&nbsp;&nbsp;&nbsp;&nbsp;まず、テストしたいコンポーネントを作ります。

```jsx
// 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;
```
&nbsp;&nbsp;&nbsp;&nbsp;そして、テストを書いてみましょう。

&nbsp;&nbsp;&nbsp;&nbsp;1.ルートの切り替え時に、正しくレンダリングされるかどうかをテストする
&nbsp;&nbsp;&nbsp;&nbsp;`React Router`をテストするには、まず`history`が必要なので、`createMemoryHistory()`を使って`history`を作成します。次に、ヘルパー関数`renderWithRouter()`を使用してコンポーネントをレンダリングし、`history`をRouterコンポーネントに渡します。これで、最初に読み込んだページがホームページであるかどうかをテストし、期待されるLinkコンポーネントをナビゲーションバーに表示することができるようになりました。

```jsx
// 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);
});
```
&nbsp;&nbsp;&nbsp;&nbsp;2.Linkをクリックすると、別のページに移動するかどうかをテストする
&nbsp;&nbsp;&nbsp;&nbsp;Linkが動作しているかどうかを確認するには、Linkのクリックイベントを発生させる必要があります。

```jsx
// 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のテスト
&nbsp;&nbsp;&nbsp;&nbsp;まず、テストしたいコンポーネントを作ります。
&nbsp;&nbsp;&nbsp;&nbsp;簡単なコンポーネントにリクエストボタンを実装します。 そして、データが利用できない場合は、メッセージ`（Loading...）`を表示します。

```jsx
// 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;
```
&nbsp;&nbsp;&nbsp;&nbsp;そして、テストを書いてみましょう。

&nbsp;&nbsp;&nbsp;&nbsp;1.データが正しく取得され、表示されていることをテストする
&nbsp;&nbsp;&nbsp;&nbsp;HTTPのリクエストを処理するためには、 `jest.mock('axios')`で`axios`リクエストをシミュレートする必要があります。あとは`axiosMock`の`get()`関数とJestの組み込み関数`mockResolvedValueOnce()`を使って、シミュレートされたデータを引数として渡します。2番目のテストでは、非同期リクエストを処理するために`async/await`を使用しなければなリません。

```jsx
// 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");
});
```
### 終わりに
&nbsp;&nbsp;&nbsp;&nbsp;ここまでで、簡単な７つのステップでほどんとReactアプリをテストできました。
&nbsp;&nbsp;&nbsp;&nbsp;React Testing Libraryを使えば、Reactアプリを完全にテストしやすくなり、テストを書くのが楽しくなり、もうコンポーネントを気にする必要がない生活を享受することができるようになります。