27
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Zeals🔥✨Advent Calendar 2020

Day 8

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

Posted at

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()は、レンダリングするコンポーネント、initialStatestoreを引数として受け取ります。storeがない場合は新しいstoreを作成し、initialStateまたはstoreを受信しなかった場合は空のオブジェクトを返します。次にrender()を使用してコンポーネントをレンダリングし、storeProviderに渡します。

// 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番目のパラメータとしてinitialStaterenderWithRedux()に渡します。

// 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リクエストをシミュレートする必要があります。あとはaxiosMockget()関数と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アプリを完全にテストしやすくなり、テストを書くのが楽しくなり、もうコンポーネントを気にする必要がない生活を享受することができるようになります。

27
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
27
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?