React Testing Library で発生する「update was not wrapped in act()」警告の原因と解決方法
React Testing Library で発生する "update was not wrapped in act()" 警告の解決方法
React Testing Library でテストを実行していると、以下のような警告が表示されることがあります。
Warning: An update inside a test was not wrapped in act(...).
When testing React components, you should wrap all updates that read rendered output with act(...).
Read more: https://testing-library.com/docs/testing-dom/act
この警告は、テスト内で状態更新が行われた際に act()
関数でラップされていない場合に発生します。act()
関数は、テスト内で実行される非同期処理をシミュレートし、テスト結果の整合性を保証するために必要なものです。
警告の解決方法
この警告を解決するには、以下のいずれかの方法で act()
関数を使用して状態更新をラップする必要があります。
方法 1: act() 関数で直接ラップする
最も単純な方法は、act()
関数で状態更新処理を直接ラップすることです。
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
test('ボタンをクリックするとカウンターがインクリメントされる', () => {
const { getByRole } = render(<Counter />);
// ボタンをクリックする
const button = getByRole('button');
userEvent.click(button);
// act() 関数で状態更新をラップする
act(() => {
// ボタンをクリックすると状態が更新される
});
// カウンターの値を確認する
const counter = screen.getByText('1');
expect(counter).toBeInTheDocument();
});
方法 2: React Testing Library のヘルパー関数を使用する
React Testing Library には、fireEvent()
や waitFor()
などのヘルパー関数が用意されており、これらの関数は内部で act()
関数を使用して状態更新をラップしています。そのため、これらのヘルパー関数を使用する場合は、act()
関数を明示的に呼び出す必要はありません。
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
test('ボタンをクリックするとカウンターがインクリメントされる', () => {
const { getByRole } = render(<Counter />);
// ボタンをクリックする
const button = getByRole('button');
userEvent.click(button);
// waitFor() ヘルパー関数を使用する
waitFor(() => {
// ボタンをクリックすると状態が更新される
});
// カウンターの値を確認する
const counter = screen.getByText('1');
expect(counter).toBeInTheDocument();
});
補足
- 複数の状態更新を行う場合は、すべて同じ
act()
関数でラップする必要があります。 act()
関数は、テストコード内でのみ使用できます。本番コードでは使用しないでください。
上記以外にも、いくつかの方法でこの警告を解決することができます。詳細は、上記の参考情報および React Testing Library ドキュメントを参照してください。
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
test('ボタンをクリックするとカウンターがインクリメントされる', () => {
const { getByRole } = render(<Counter />);
// ボタンをクリックする
const button = getByRole('button');
userEvent.click(button);
// act() 関数で状態更新をラップする
act(() => {
// ボタンをクリックすると状態が更新される
});
// カウンターの値を確認する
const counter = screen.getByText('1');
expect(counter).toBeInTheDocument();
});
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
test('ボタンをクリックするとカウンターがインクリメントされる', () => {
const { getByRole } = render(<Counter />);
// ボタンをクリックする
const button = getByRole('button');
userEvent.click(button);
// waitFor() ヘルパー関数を使用する
waitFor(() => {
// ボタンをクリックすると状態が更新される
});
// カウンターの値を確認する
const counter = screen.getByText('1');
expect(counter).toBeInTheDocument();
});
例 3: useEffect フックを使用する
useEffect
フックを使用して、状態更新をテストのライフサイクルの外に実行することもできます。
import React, { useState } from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
test('ボタンをクリックするとカウンターがインクリメントされる', () => {
const [count, setCount] = useState(0);
const { getByRole } = render(<Counter count={count} setCount={setCount} />);
// ボタンをクリックする
const button = getByRole('button');
userEvent.click(button);
// useEffect フックを使用して状態更新を行う
useEffect(() => {
setCount(count + 1);
}, [count]);
// カウンターの値を確認する
const counter = screen.getByText('1');
expect(counter).toBeInTheDocument();
});
説明
上記の例では、シンプルなカウンターコンポーネントと、そのコンポーネントをテストするコードを示しています。
- 例 1 では、
act()
関数を使用して、ボタンクリック後に発生する状態更新をラップしています。 - 例 2 では、
waitFor()
ヘルパー関数を使用して、状態更新が完了するのを待機しています。
これらの例は、"update was not wrapped in act()" 警告を解決するためのほんの一例です。状況に応じて、最適な方法を選択してください。
act()
関数とuseEffect
フックの詳細については、React ドキュメントを参照してください。
React Testing Library で "update was not wrapped in act()" 警告を解決するその他の方法
asynchronous オプションを使用する
render
関数の asynchronous
オプションを使用すると、テスト内のすべての更新が自動的に act()
関数でラップされます。これは、テストコードを簡潔にするのに役立ちます。
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
test('ボタンをクリックするとカウンターがインクリメントされる', async () => {
const { getByRole } = render(<Counter />, { asynchronous: true });
// ボタンをクリックする
const button = getByRole('button');
userEvent.click(button);
// カウンターの値を確認する
const counter = screen.getByText('1');
expect(counter).toBeInTheDocument();
});
jest.useFakeTimers
を使用すると、テスト内で時間経過をシミュレートできます。これにより、非同期処理が完了するのを待つことなく、状態更新をアサーションできます。
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import jest from 'jest-mock';
test('ボタンをクリックするとカウンターがインクリメントされる', () => {
jest.useFakeTimers();
const { getByRole } = render(<Counter />);
// ボタンをクリックする
const button = getByRole('button');
userEvent.click(button);
// jest.advanceTime() で時間を進める
jest.advanceTime(100);
// カウンターの値を確認する
const counter = screen.getByText('1');
expect(counter).toBeInTheDocument();
});
react-dom/test
パッケージには、act
関数と同様の機能を持つ flush
関数が含まれています。この関数は、テスト内のすべての更新を強制的にフラッシュし、アサーションを実行する前に完了することを保証します。
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { flush } from 'react-dom/test';
test('ボタンをクリックするとカウンターがインクリメントされる', () => {
const { getByRole } = render(<Counter />);
// ボタンをクリックする
const button = getByRole('button');
userEvent.click(button);
// flush() 関数で更新をフラッシュする
flush();
// カウンターの値を確認する
const counter = screen.getByText('1');
expect(counter).toBeInTheDocument();
});
注意事項
上記の方法を使用する場合は、それぞれの方法の注意事項と制限事項を理解する必要があります。
asynchronous
オプションは、すべての更新がact()
関数でラップされるため、テストコードが冗長になる可能性があります。jest.useFakeTimers
は、テストコードを複雑にする可能性があります。react-dom/test
パッケージは、React Testing Library の一部ではないため、互換性の問題が発生する可能性があります。
reactjs unit-testing react-hooks