メインコンテンツへスキップ

FAQ

Reactのテストに特有ではない質問については、メインのFAQも参照してください。

inputのonChangeハンドラーをテストするにはどうすればよいですか?

要約:on-change.jsの例を参照してください

要約すると

import React from 'react'
import {render, fireEvent} from '@testing-library/react'

test('change values via the fireEvent.change method', () => {
const handleChange = jest.fn()
const {container} = render(<input type="text" onChange={handleChange} />)
const input = container.firstChild
fireEvent.change(input, {target: {value: 'a'}})
expect(handleChange).toHaveBeenCalledTimes(1)
expect(input.value).toBe('a')
})

test('select drop-downs must use the fireEvent.change', () => {
const handleChange = jest.fn()
const {container} = render(
<select onChange={handleChange}>
<option value="1">1</option>
<option value="2">2</option>
</select>,
)
const select = container.firstChild
const option1 = container.getElementsByTagName('option').item(0)
const option2 = container.getElementsByTagName('option').item(1)

fireEvent.change(select, {target: {value: '2'}})

expect(handleChange).toHaveBeenCalledTimes(1)
expect(option1.selected).toBe(false)
expect(option2.selected).toBe(true)
})

test('checkboxes (and radios) must use fireEvent.click', () => {
const handleChange = jest.fn()
const {container} = render(<input type="checkbox" onChange={handleChange} />)
const checkbox = container.firstChild
fireEvent.click(checkbox)
expect(handleChange).toHaveBeenCalledTimes(1)
expect(checkbox.checked).toBe(true)
})

enzymeやReactのTestUtilsを使用したことがある場合、次のように入力を変更することに慣れているかもしれません。

input.value = 'a'
Simulate.change(input)

React Testing Libraryでは、Reactがinputvalueプロパティを代入するたびにそれを追跡しているため、changeイベントを発行すると、Reactは値が実際には変更されていないと考えます。したがって、これを行うことはできません。

これは、Simulateが内部APIを使用して特別なシミュレートされたイベントを発行するため機能します。React Testing Libraryでは、テストをより柔軟にするために実装の詳細を回避しようとしています。

そのため、Reactが追跡できない方法でchangeイベントハンドラーがプロパティを設定するように調整しました。これが、changeメソッド呼び出しの一部として値を渡す必要がある理由です。

このライブラリで単体テストを書くことはできますか?

もちろんです!このライブラリを使用して単体テストと統合テストを書くことができます。(このライブラリは意図的にshallowレンダリングをサポートしていないため)高レベルのコンポーネントを単体テストしたい場合は、依存関係をモックする方法の詳細については下記をご覧ください。このプロジェクトのテストでは、このライブラリを使用した単体テストの例をいくつか示しています。

テストを作成する際には、次の点に留意してください。

テストがソフトウェアの使用方法に似ているほど、より確信を持ってテストを実施できます。- 2018年2月17日

コンポーネントまたはフックでスローされたエラーをテストするにはどうすればよいですか?

コンポーネントがレンダリング中に例外をスローした場合、状態更新の起点もactでラップされていれば例外をスローします。デフォルトでは、renderfireEventactでラップされています。try-catchでラップするか、テストランナーがサポートしている場合は専用のマッチャーを使用できます。たとえば、JestではtoThrowを使用できます。

function Thrower() {
throw new Error('I throw')
}

test('it throws', () => {
expect(() => render(<Thrower />)).toThrow('I throw')
})

これはフックとrenderHookにも当てはまります。

function useThrower() {
throw new Error('I throw')
}

test('it throws', () => {
expect(() => renderHook(useThrower)).toThrow('I throw')
})
情報

React 18は、拡張エラーメッセージでconsole.errorを呼び出します。React 19は、状態の更新がactでラップされていない限り、拡張エラーメッセージでconsole.warnを呼び出します。renderrenderHookfireEventはデフォルトでactでラップされます。

shallowレンダリングを使用できない場合、テストでコンポーネントをモックするにはどうすればよいですか?

一般的に、コンポーネントをモックアウトすることは避ける必要があります(基本原則のセクションを参照)。ただし、必要な場合は、Jestのモック機能を使用してみてください。モックが特に便利なのは、アニメーションライブラリの場合です。テストでアニメーションが終了するのを待ちたくありません。

jest.mock('react-transition-group', () => {
const FakeTransition = jest.fn(({children}) => children)
const FakeCSSTransition = jest.fn(props =>
props.in ? <FakeTransition>{props.children}</FakeTransition> : null,
)
return {CSSTransition: FakeCSSTransition, Transition: FakeTransition}
})

test('you can mock things with jest.mock', () => {
const {getByTestId, queryByTestId} = render(
<HiddenMessage initialShow={true} />,
)
expect(queryByTestId('hidden-message')).toBeTruthy() // we just care it exists
// hide the message
fireEvent.click(getByTestId('toggle-message'))
// in the real world, the CSSTransition component would take some time
// before finishing the animation which would actually hide the message.
// So we've mocked it out for our tests to make it happen instantly
expect(queryByTestId('hidden-message')).toBeNull() // we just care it doesn't exist
})

Jestのモック関数(jest.fn())であるため、必要に応じて、それらに対するアサーションも行うことができます。

完全な例については、完全なテストを開くを参照してください。

これはshallowレンダリングよりも多くの作業のように見えます(実際そうです)が、モックがモックしているものに十分に似ていれば、より確信を持つことができます。

shallowレンダリングに似たものにしたい場合は、このようにすることができます。

Jestのモックの仕組みの詳細については、ブログ記事「でも、JavaScriptモックとは一体何ですか?」をご覧ください。

enzymeの何が「複雑さと機能で肥大化しており」、「不適切なテスト手法を推奨している」のでしょうか?

有害な機能のほとんどは、実装の詳細なテストを推奨することに関係しています。主に、shallowレンダリング、コンポーネントコンストラクターでレンダリングされた要素を選択できるAPI、およびコンポーネントインスタンス(とその状態/プロパティ)を取得して操作できるAPI(enzymeのラッパーAPIのほとんどがこれを許可しています)です。

このライブラリの基本原則は次のとおりです。

テストがソフトウェアの使用方法に似ているほど、より確信を持ってテストを実施できます。- 2018年2月17日

ユーザーはアプリのコンポーネントインスタンスを直接操作したり、内部状態やレンダリングするコンポーネントをアサートしたり、内部メソッドを呼び出すことができないため、テストでこれらのことを行うと、テストがもたらすことができる信頼性が低下します。

これらを実行するユースケースが絶対にないと言うのではなく、これらの実行を可能にする必要があり、reactコンポーネントをテストするためのデフォルトの自然な方法ではありません。

スナップショットの差分が機能しないのはなぜですか?

snapshot-diffライブラリを使用してスナップショットの差分を保存する場合、このライブラリは変更可能なDOMを使用するため、そのままでは機能しません。変更は新しいオブジェクトを返さないため、snapshot-diffは同じオブジェクトであるとみなし、差分の比較を回避します。

幸い、これを機能させる簡単な方法があります。snapshot-diffに渡すときにDOMを複製します。次のようになります。

const firstVersion = container.cloneNode(true)
// Do some changes
snapshotDiff(firstVersion, container.cloneNode(true))
「更新がact(...)でラップされていません」という警告を修正するにはどうすればよいですか?

この警告は通常、テストがすでに終了した後に更新を引き起こす非同期操作によって発生します。これを解決するには2つの方法があります。

  1. 非同期ユーティリティの1つ(waitForfind*クエリなど)を使用して、テストで操作の結果を待ちます。例:const userAddress = await findByLabel(/address/i)
  2. 状態の更新をトリガーしないように非同期操作をモックアウトします。

一般的に言って、アプローチ1は、アプリを操作するユーザーの期待によく一致するため、推奨されます。

また、このブログ記事は、信頼できるテストを書き、これらの警告を回避する方法を検討する際に役立つ場合があります。

コンポーネントツリーのどのレベルをテストする必要がありますか?子、親、または両方?

このライブラリの基本原則に従うと、テストは特定のコンポーネント自体ではなく、ユーザーがアプリケーションの機能をどのように体験し、操作するかに基づいて編成することが役立ちます。場合によっては、再利用可能なコンポーネントライブラリの場合、テスト対象のユーザーのリストに開発者を含めて、再利用可能な各コンポーネントを個別にテストすることが役立つ場合があります。その他の場合、コンポーネントツリーの具体的な分割は実装の詳細にすぎず、そのツリー内のすべてのコンポーネントを個別にテストすると問題が発生する可能性があります(https://kentcdodds.com/blog/avoid-the-test-userを参照)。

実際には、これは多くの場合、現実的なユーザーインタラクションをシミュレートするために、コンポーネントツリーの上位でテストすることが望ましいことを意味します。これに加えて、上位または下位のレベルでさらにテストする価値があるかどうかは、トレードオフの問題と、コストに見合う十分な価値を提供するものにかかっています(さまざまなレベルのテストの詳細については、https://kentcdodds.com/blog/unit-vs-integration-vs-e2e-testsを参照してください)。

このトピックの詳細な議論については、このビデオを参照してください。