React Testing LibraryでReactコンポーネントを基本的なテストをする方法

React Testing Libraryで基本的なReactコンポーネントをテストする使い方を説明します。

React Testing Libraryとは

(確認したバージョン 15.0.2 です)

Reactコンポーネントをレンダーして要素をテストする方法

与えられたpropsに対してコンポーネントが正しくレンダリングされるかをテストしたいことはよくあります。 renderコンポーネントレンダリングscreenのクエリ系のメソッドで要素を取得し、expectアサーションをします。

また、@testing-library/jest-domを使うことで、DOM要素に対する便利なマッチャーを使えます。よく使うのは、次のようなメソッドです。

メソッド 説明
toBeInTheDocument() 要素が存在するか
toHaveTextContent(...) 要素のテキストが一致するか
toHaveAttribute(...) 要素の属性が一致するか
// hello.test.jsx
import React from 'react'
import '@testing-library/jest-dom';
import { render, screen } from "@testing-library/react";

import Hello from "./hello";

describe('Hello', () => {
  it('renders "Hey, stranger"', () => {
    // renderでコンポーネントをレンダリング
    render(<Hello />);
    // screen経由で要素を取得し、アサーションをする
    expect(screen.getByText('Hey, stranger')).toBeInTheDocument();
  });

  it('renders "Hello, John" with the name props', () => {
    render(<Hello name="John" />);
    expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent('Hello, John');
  });
});
// hello.jsx - テスト対象のコンポーネント
import React from "react";

export default function Hello(props) {
  if (props.name) {
    return <h1>Hello, {props.name}!</h1>;
  } else {
    return <span>Hey, stranger</span>;
  }
}

要素を取得する方法

React Testing Libraryで要素を取得するために、

  • getBy (複数の要素の場合は getAllBy
  • findBy (複数の要素の場合は findAllBy
  • queryBy (複数の要素の場合は queryAllBy

また、要素の取得方法も、次のように様々な方法がサポートされています。

  • Role:role属性で要素を取得
  • LabelText : ラベルのテキストで要素を取得
  • PlaceholderText: プレースホルダーのテキストで要素を取得
  • Text : テキストで要素を取得
  • DisplayValue:表示されている値で要素を取得
  • AltText:alt属性で要素を取得
  • Title:title属性で要素を取得。SVGなどで利用
  • TestId:data-testid属性で要素を取得:

これらを組み合わせて、getByRolequeryAllByTextなど目的に合わせて使い分けることができます。

getBy / findBy / queryBy の使い分けとしては、要素がある場合は、getBy、要素を待つ場合はfindBy、要素がない場合はqueryByを使うと良いでしょう。

またgetAllBy / findAllBy / queryAllBy は、複数の要素を取得する場合に使います。こちらも同様に、要素がある場合は、getAllBy、要素を待つ場合はfindAllBy、要素がない場合はqueryAllByを使うと良いでしょう。

各メソッドの早見表は次のとおりです。

メソッド 要素が0件 要素が1件 要素が複数 Waitするか?
getBy エラー 1要素 エラー No
findBy エラー 1要素 エラー Yes
queryBy null 1要素 エラー No
getAllBy エラー 配列 配列 No
findAllBy エラー 配列 配列 Yes
queryAllBy [] 配列 配列 No

参考: Testing LibraryのQueryについて

Reactコンポーネントのインタラクションテストをする方法

コンポーネントに対して、ユーザー操作をテストしたいこともよくあります。@testing-library/user-eventを使うことでユーザー操作をシミュレートすることができます。(他の方法もあります)

import React from 'react';
import '@testing-library/jest-dom';
import userEvent from '@testing-library/user-event'
import { render, screen } from "@testing-library/react";

import Toggle from "./toggle";

describe('Toggle', () => {
  it('renders "Turn on" button', () => {
    const onChange = jest.fn();
    render(<Toggle onChange={onChange} />);

    expect(screen.getByRole('button')).toHaveTextContent('Turn on');
    expect(onChange).not.toHaveBeenCalled();
  });

  it('renders "Turn off" button after click and called onChange', async () => {
    const onChange = jest.fn();
    // userEvent.setup()でユーザーイベントをセットアップ
    // コンポーネントをレンダリングする前に呼び出すのを推奨
    const user = userEvent.setup();
    render(<Toggle onChange={onChange} />);

    const button = screen.getByRole('button')
    // ボタンクリックをシミュレート
    await user.click(button)

    expect(button).toHaveTextContent('Turn off');
    expect(onChange).toHaveBeenCalledWith(true);
  });

  // ...
});
// toggle.jsx - テスト対象のコンポーネント
import React, { useState } from "react";

export default function Toggle({ onChange }) {
  const [state, setState] = useState(false);

  return (
    <button
      onClick={() => {
        setState(previousState => !previousState);
        onChange(!state);
      }}
    >
      {state === true ? "Turn off" : "Turn on"}
    </button>
  );
}

APIから非同期でデータ取得するコンポーネントをテストする

APIからデータを非同期で取得して、その内容を表示するコンポーネントはよくあります。このようなコンポーネントをテストする場合、jest.mockを使ってAPIのレスポンスをモック化し、waitForを使って非同期処理が終わるまで待つことができます。

// posts.tests.jsx
import React from 'react'
import '@testing-library/jest-dom';
import { render, screen, waitFor } from "@testing-library/react";
import axios from 'axios';

import Posts from './posts';

jest.mock('axios');

describe('Posts', () => {
  it('renders the posts', async () => {
    // axios.getをモック化
    const fakePosts = [
      { id: 1, title: 'First post' },
      { id: 2, title: 'Second post' },
    ];
    axios.get.mockImplementation(() =>
      Promise.resolve({
        json: () => Promise.resolve(fakePosts)
      })
    );

    render(<Posts />);

    // waitForで"loading..."が表示されなくなることを待つ
    await waitFor(() => {
      expect(screen.queryByText(/loading.../)).not.toBeInTheDocument();
    });
    expect(screen.getByText('First post')).toBeInTheDocument();
    expect(screen.getByText('Second post')).toBeInTheDocument();
  });
});
// posts.jsx - テスト対象のコンポーネント
import React, { useState, useEffect } from "react";
import axios from "axios";

export default function Posts() {
  const [posts, setPosts] = useState([]);
  const [isLoading, setIsLoading] = useState(true);

  const fetchPosts = async () => {
    const response = await axios.get('/posts').then((res) => res.json());
    console.log(response);
    setPosts(response);
    setIsLoading(false);
  };

  useEffect(() => {
    fetchPosts();
  }, []);

  if (isLoading) {
    return "loading...";
  }

  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

React Hooksのテスト方法

React Hooksをテストしたい場合は、renderHookを使います。renderHookはHooksをテストするためのユーティリティです。

// use-logged-in-user.test.js
import { renderHook } from '@testing-library/react';
import { waitFor } from "@testing-library/react";
import axios from 'axios';

import useLoggedInUser from './use-logged-in-user';

// Jestでaxiosをモック化
jest.mock('axios');

describe('useLoggedInUser', () => {
  it('returns logged in user when logged in', async () => {
    axios.get.mockResolvedValue({ name: 'Alice' });

    // renderHookでHooksを実行
    const { result } = renderHook(() => useLoggedInUser());

    // Hooksの処理が非同期なので処理が終わるまでwaitForで待つ
    await waitFor(() => {
      expect(result.current.isLoading).toEqual(false)
    });
    expect(result.current.user).toEqual({ name: 'Alice' });
  });

  it('returns null when not logged in', async () => {
    axios.get.mockResolvedValue(null);

    // renderHookでHooksを実行
    const { result } = renderHook(() => useLoggedInUser());

    // Hooksの処理が非同期なので処理が終わるまでwaitForで待つ
    await waitFor(() => {
      expect(result.current.isLoading).toEqual(false)
    });
    expect(result.current.user).toEqual(null);
  });
});
// use-logged-in-user.js
import { useState, useEffect } from 'react';
import axios from 'axios';

export default function useLoggedInUser() {
  const [user, setUser] = useState(null);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    axios.get('/api/user')
      .then((data) => {
        setUser(data)
        setIsLoading(false)
      })
      .catch(() => {
        setIsLoading(false)
      });
  }, []);

  return { isLoading, user };
};

参考: renderHookのAPI仕様

スナップショットテスト

JestのtoMatchSnapshotを使うことで、コンポーネントレンダリング結果をスナップショットとして保存し、次回のテスト時にスナップショットと比較することができます。

// hello.test.jsx
import React from 'react'
import '@testing-library/jest-dom';
import { render, screen } from "@testing-library/react";

import Hello from "./hello";

describe('Hello', () => {
  it('renders Hello component', () => {
    const { asFragment } = render(<Hello />);

    // スナップショットを作成
    expect(asFragment()).toMatchSnapshot();
  })

  // ...
});

作成されたスナップショットは次のようになります。今後Helloコンポーネントを修正して、スナップショットと差分が出てくる場合は、スナップショットテストが失敗します。スナップショットの更新はjest --updateSnapshotで行うことができます。

// __snapshots__/hello.test.jsx.snap
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Hello renders Hello component 1`] = `
<DocumentFragment>
  <span>
    Hey, stranger
  </span>
</DocumentFragment>
`;

より高度なことをしたい場合