0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

JestとReact Testing Library(RTL)メモ

Posted at

JestとReact Testing Library(RTL)入門

このドキュメントでは、JestとReact Testing Library(RTL)を使用したReactコンポーネントのテストの基本について解説します。

1. Jestとは?

Jestは、Facebookによって開発されたJavaScriptのテストフレームワークです。特にReactアプリケーションのテストで広く利用されています。

主な特徴

  • ゼロコンフィグ: 多くのプロジェクトで設定不要で動作します。
  • スナップショットテスト: UIコンポーネントの変更を簡単に追跡できます。
  • モック機能: 外部モジュールやAPIを簡単にモックできます。
  • 高速な実行: テストを並列で実行し、高速なフィードバックを提供します。

インストール

npm install --save-dev jest babel-jest @babel/preset-env @babel/preset-react

設定

プロジェクトのルートに jest.config.js を作成します。

module.exports = {
  testEnvironment: 'jsdom',
  transform: {
    '^.+\\.(js|jsx)$': 'babel-jest',
  },
};

また、Babelの設定ファイル .babelrc も必要です。

{
  "presets": ["@babel/preset-env", "@babel/preset-react"]
}

簡単なテストの例

sum.js

function sum(a, b) {
  return a + b;
}
module.exports = sum;

sum.test.js

const sum = require('./sum');

test('adds 1 + 2 to equal 3', () => {
  expect(sum(1, 2)).toBe(3);
});

2. React Testing Library(RTL)とは?

React Testing Libraryは、ユーザーの視点に立ったテストを推奨するテストライブラリです。コンポーネントの内部実装ではなく、ユーザーがどのようにコンポーネントを操作するかに焦点を当てます。

主な特徴

  • ユーザー中心のテスト: getByText, getByRole など、ユーザーがUIをどのように認識するかに基づいたクエリを提供します。
  • 実装の詳細を避ける: コンポーネントの内部状態やpropsに直接アクセスすることを避け、より堅牢なテストを作成できます。
  • アクセシビリティ: アクセシビリティを意識したクエリが用意されており、自然とアクセシブルなコンポーネント開発につながります。

インストール

npm install --save-dev @testing-library/react @testing-library/jest-dom

@testing-library/jest-dom は、DOMの状態を検証するためのカスタムマッチャーを提供します(例: toBeInTheDocument())。

設定

jest.config.jssetupFilesAfterEnv を追加して、カスタムマッチャーを読み込みます。

// jest.config.js
module.exports = {
  // ...他の設定
  setupFilesAfterEnv: ['<rootDir>/setupTests.js'],
};

setupTests.js

import '@testing-library/jest-dom';

3. Reactコンポーネントのテスト

Button コンポーネントを例に、RTLを使ったテストの書き方を見ていきましょう。

Button.js

import React from 'react';

const Button = ({ onClick, children }) => {
  return (
    <button onClick={onClick}>
      {children}
    </button>
  );
};

export default Button;

Button.test.js

import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import Button from './Button';

describe('Button component', () => {
  test('renders button with text', () => {
    render(<Button>Click me</Button>);
    
    // `screen.getByText` を使ってテキストで要素を取得
    const buttonElement = screen.getByText(/click me/i);
    expect(buttonElement).toBeInTheDocument();
  });

  test('calls onClick handler when clicked', () => {
    const handleClick = jest.fn();
    render(<Button onClick={handleClick}>Click me</Button>);
    
    const buttonElement = screen.getByText(/click me/i);
    
    // `fireEvent` を使ってクリックイベントを発生させる
    fireEvent.click(buttonElement);
    
    // `handleClick` が1回呼び出されたことを確認
    expect(handleClick).toHaveBeenCalledTimes(1);
  });
});

主なRTLのAPI

  • render(): コンポーネントをDOMにレンダリングします。
  • screen: レンダリングされたDOMにアクセスするためのクエリを提供します。
  • fireEvent: クリックや入力などのユーザーイベントを発生させます。
  • jest.fn(): モック関数を作成し、関数の呼び出しを追跡します。

4. JestとRTLの役割分担

JestとRTLは競合するものではなく、協調して動作します。それぞれの役割を理解することが重要です。

  • Jest:

    • テストランナー: テストファイルを見つけて実行し、結果を報告します。
    • アサーションライブラリ: expect().toBe() などの関数を提供し、値が期待通りか検証します。
    • モック機能: jest.fn()jest.mock() を提供し、関数やモジュールを偽のバージョンに置き換えます。テスト環境をコントロールするために不可欠です。
  • React Testing Library (RTL):

    • コンポーネントのレンダリング: render() 関数でテスト対象のコンポーネントを仮想DOMに描画します。
    • DOMクエリ: screen.getByRole() のように、ユーザーがUIをどのように認識するかに基づいて要素を検索する手段を提供します。
    • ユーザーイベントの発火: fireEvent@testing-library/user-event を使って、クリックやキーボード入力などのユーザー操作をシミュレートします。

結論: Jestがテスト全体のフレームワーク(実行、検証、モック)を提供し、RTLはその上でReactコンポーネントをユーザー視点でテストするための便利なツールセットを提供する、という関係性です。

5. より実践的なテストシナリオ

シナリオ1: フォームの入力と送信

ユーザーがフォームに入力し、送信ボタンを押すという一連の操作をテストします。

LoginForm.js

import React, { useState } from 'react';

const LoginForm = ({ onSubmit }) => {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    onSubmit({ email, password });
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor="email">Email</label>
        <input
          id="email"
          type="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
        />
      </div>
      <div>
        <label htmlFor="password">Password</label>
        <input
          id="password"
          type="password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
        />
      </div>
      <button type="submit">Login</button>
    </form>
  );
};

export default LoginForm;

LoginForm.test.js

import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
// fireEventよりもユーザーの操作をより忠実に再現するuser-eventの利用を推奨
import userEvent from '@testing-library/user-event';
import LoginForm from './LoginForm';

test('allows the user to log in', async () => {
  const handleSubmit = jest.fn();
  render(<LoginForm onSubmit={handleSubmit} />);

  // userEventを使って入力操作をシミュレート
  await userEvent.type(screen.getByLabelText(/email/i), 'test@example.com');
  await userEvent.type(screen.getByLabelText(/password/i), 'password');

  // 送信ボタンをクリック
  await userEvent.click(screen.getByRole('button', { name: /login/i }));

  // onSubmitが正しい引数で呼び出されたか検証
  expect(handleSubmit).toHaveBeenCalledWith({
    email: 'test@example.com',
    password: 'password',
  });
  expect(handleSubmit).toHaveBeenCalledTimes(1);
});

ポイント: fireEvent よりも @testing-library/user-event を使うと、実際のユーザー操作(キー入力のタイミングなど)をより正確にシミュレートでき、推奨されています。

シナリオ2: API通信と非同期処理

コンポーネントがマウントされた時にAPIを叩き、取得したデータを表示するケースです。ここではaxiosを使ったAPI通信をjest.mock()でモックします。

UserProfile.js

import React, { useState, useEffect } from 'react';
import axios from 'axios';

const UserProfile = ({ userId }) => {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const fetchUser = async () => {
      try {
        const response = await axios.get(`https://api.example.com/users/${userId}`);
        setUser(response.data);
      } catch (error) {
        console.error('Failed to fetch user');
      } finally {
        setLoading(false);
      }
    };
    fetchUser();
  }, [userId]);

  if (loading) {
    return <div>Loading...</div>;
  }

  if (!user) {
    return <div>User not found</div>;
  }

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
};

export default UserProfile;

UserProfile.test.js

import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import axios from 'axios';
import UserProfile from './UserProfile';

// axiosモジュール全体をモック化
jest.mock('axios');

test('fetches and displays user data', async () => {
  const mockUser = { name: 'John Doe', email: 'john.doe@example.com' };
  
  // モックされたaxios.getが解決する値を設定
  axios.get.mockResolvedValue({ data: mockUser });

  render(<UserProfile userId="1" />);

  // 初期表示(ローディング)の確認
  expect(screen.getByText(/loading/i)).toBeInTheDocument();

  // `findBy`系のクエリは非同期で要素が出現するのを待つ
  const userName = await screen.findByText('John Doe');
  expect(userName).toBeInTheDocument();
  
  // `getBy`は同期的。この時点では要素は存在しているはず
  expect(screen.getByText('john.doe@example.com')).toBeInTheDocument();

  // ローディング表示が消えたことを確認
  expect(screen.queryByText(/loading/i)).not.toBeInTheDocument();
});

ポイント:

  • jest.mock('axios')axiosからのimportをモックに差し替えます。
  • mockResolvedValueで非同期APIの成功レスポンスを定義します。
  • findByTextwaitForユーティリティを使い、非同期でUIが更新されるのを待ちます。getByは同期的、queryByは要素がなくてもエラーを投げない、findByは非同期で待つ、という違いがあります。

まとめ

JestとReact Testing Libraryを組み合わせることで、単純なUIの表示からフォーム操作、非同期通信といった複雑なシナリオまで、Reactアプリケーションの品質を高く保つための堅牢なテストスイートを構築できます。ユーザーの操作をシミュレートすることで、リファクタリングに強く、信頼性の高いテストを書くことを目指しましょう。

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?