Unit Test - smiyawaki0820/tutorial-react GitHub Wiki

単体テストとは

  • プログラムを構成する小さな単位(ユニット)が個々の機能を正しく果たしているかどうかを検証するテスト
  • モジュールの結合前にテストを実施するため、問題の特定や修正が容易
  • 開発者によって、コード作成直後にテストケースが作成されるため、妥当性の高いテストケースを資産として残すことができる
  • ユニットテスト実行環境での役割
    • テストランナー:ユニットテストファイルの実行処理を行う
    • アサーションライブラリ:値の比較・判定を行う
    • モックライブラリ:テスト環境ようのダミーオブジェクトを準備する

Jest

TBA

  • create-react-app を使用している場合、標準でインストールされる
  • npm test で実行する
  • Jestはテストランナーであり、コマンドラインからJestでテストを実行する能力を提供します。
import sum from './math.js';

describe('sum', () => {
  test('sums up two values', () => {
    expect(sum(2, 4)).toBe(6);
  });
});

@testing-library/react

  • create-react-app を使用している場合、標準でインストールされる
  • 以下に対するテストの実装は避ける:
    • コンポーネントの内部状態
    • コンポーネントの内部メソッド
    • コンポーネントのライフサイクル
    • 子コンポーネント

テスト例(デフォルトの場合)

// src/App.tsx
import React from 'react';
import logo from './logo.svg';
import './App.css';

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.tsx</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
}

export default App;
// src/App.test.tsx
import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';

test('renders learn react link', () => {
  // 対象コンポーネントのレンダリング
  render(<App />);
  // screen: レンダしたコンポーネント内の要素にアクセスする際に用いる
  // - getByText: 
  // - getByRole: セレクタにアクセスする
  const linkElement = screen.getByText(/learn react/i);
  // expect: 検証対象に対するテストを行うための
  // - toBeInTheDocument(): 
  // - toBeDisabled(): ボタン要素が disabled になっているか検証
  expect(linkElement).toBeInTheDocument();
});
$ npm test
スクリーンショット 2022-04-21 11 47 23
  • getBy*:クエリに対してマッチするノードを返す。マッチする要素がない・複数ある場合エラーメッセージを表示する(マッチング候補が複数ある場合は getAllBy* を用いる)。
  • queryBy*:クエリに対してマッチするノードを返す。マッチする要素がない場合は null を返すため、存在しない要素をアサーションするのに用いられる(マッチング候補が複数ある場合は queryAllBy* を用いる)。
  • findBy*:クエリにマッチする要素が見つかった際に解決のための Promise を返す。要素が見つからない場合やタイムアウト(default=1秒)後に複数の要素が見つかった場合は Promise が拒否される。

スクリーンショット 2022-04-21 14 34 08

テストにおける優先事項

1. Queries Accessible to Everyone

  • ByRole:アクセス可能な DOM 要素に対する検索を行う。
  • ByLabelText:主にフォームフィールドのラベルテキストに対して使用される。
  • ByPlaceholderText:主にフォームフィールドのプレースホルダーに対して使用される。 スクリーンショット 2022-04-21 14 54 51
  • ByText:主に非インタラクティブな要素(div, span, paragraph など)を見つけるために使用される。
  • ByDisplayValue:フォーム要素における値は、値が記入されたページに対して有効。

2. Semantic Queries

  • ByAltText:主に要素が alt テキストをサポートするもの(img, area, input など)に使用される。
  • ByTitle:screan リーダーで読み取ることができない title 属性に対して使用される。

3. Test IDs

  • ByTestId:ユーザはこれらを見れないため重要性は低いが、上記でマッチできないものに対して使用される。
import {screen, getByLabelText} from '@testing-library/dom'

// screen を使用する場合(単に document.body にアクセスするだけなら screen を推奨)
const inputNode1 = screen.getByLabelText('Username')

// screen を使用せずにコンテナを使用する場合
const container = document.querySelector('#app')
const inputNode2 = getByLabelText(container, 'Username')
// 文字列マッチ
screen.getByText('Hello World') // full string match
screen.getByText('llo Worl', {exact: false}) // substring match
screen.getByText('hello world', {exact: false}) // ignore case

// 正規表現
screen.getByText(/World/) // substring match
screen.getByText(/world/i) // substring match, ignore case
screen.getByText(/^hello world$/i) // full string match, ignore case
screen.getByText(/Hello W?oRlD/i) // substring match, ignore case, searches for "hello world" or "hello orld"

// カスタム関数
screen.getByText((content, element) => content.startsWith('Hello'))

// 正規化
screen.getByText('text', {
  normalizer: getDefaultNormalizer({trim: false}),   // trim, collapseWhitespace など
})

User Actions

Basic Hooks

import { useState, useCallback } from 'react'

// 初期値を props として渡すことでテストが容易になる
export default function useCounter(initialValue = 0) {
  const [count, setCount] = useState(initialValue)
  const increment = useCallback(() => setCount((x) => x + 1), [])
  const reset = useCallback(() => setCount(initialValue), [initialValue])
  return { count, increment, reset }
}
import { renderHook } from '@testing-library/react-hooks'
import useCounter from './useCounter'

test('should use counter', () => {
  let initialValue = 0
  const { result, rerender } = renderHook(() => useCounter(initialValue))

  expect(result.current.count).toBe(initialValue)
  expect(typeof result.current.increment).toBe('function')

  // update
  act(() => {                   // フックがブラウザ上でどのように動作するかシミュレート
    result.current.increment()  // 値を更新
  })

  expect(result.current.count).toBe(initialValue+1)

  initialValue = 10
  rerender()

  act(() => {
    result.current.reset()
  })

  expect(result.current.count).toBe(10)
})

Advanced Hooks

  • テストが特定タイプの環境を必要とする場合は、以下のように区別する

import { renderHook, act } from '@testing-library/react-hooks'        // will attempt to auto-detect
import { renderHook, act } from '@testing-library/react-hooks/dom'    // will use react-dom
import { renderHook, act } from '@testing-library/react-hooks/native' // will use react-test-renderer
import { renderHook, act } from '@testing-library/react-hooks/server' // will use react-dom/server
⚠️ **GitHub.com Fallback** ⚠️