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?

TL;DR

  • Reactのコンポーネントテストにおいて、actを正しく処理しないとFlaky Test(不安定なテスト)になる
  • 非同期処理や状態更新の完了を待たずにアサーション(検証)すると、テスト結果が毎回変化する
  • actを使わずに、waitForfindByなどのラッパー関数を使うのが安全

はじめに

React Testing Libraryをつかって、コンポーネントをテストしているとこんなWarningに遭遇しました。

An update to SimpleButton inside a test was not wrapped in act(...).

When testing, code that causes React state updates should be wrapped into act(...):

act(() => {
  /* fire events that update state */
});
  /* assert on the output */

This ensures that ...(以下略)

要約すると、
「あんたのテスト、状態が正しく変更されてないままテスト終了してるで。ほんまに大丈夫?」
というWarningです。

このWarning自体はごもっともなメッセージなのですが、問題はactで囲めということです。

actとは何か

Reactの公式サイトによると、

act() というヘルパーは、あなたが何らかのアサーションを行う前に、これらの「ユニット」に関連する更新がすべて処理され、DOM に反映されていることを保証します。

言い換えると、Reactの状態更新や副作用(useEffectなど)をまとめて完了させてからアサーションを実行するためのヘルパー関数です。

actによりFlaky Testになる3つの理由

1. 非同期処理の完了を正しく待てていない

なぜFlaky Testになる?

  • タイミングの問題: 非同期処理の完了タイミングがネットワーク状況やCPU負荷によって毎回変わる
  • 競合状態: 状態更新とアサーションが同時に実行され、どちらが先に完了するかが予測できない

悪い例

test('データ取得後にローディングが消える', async () => {
  render(<DataComponent />);
  
  // この時点でfetchDataが完了していない可能性がある
  act(async () => {
    await fetchData();
  });
  
  // fetchDataが完了する前にアサーションが実行される可能性
  expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
});

良い例

awaitをつける。

test('データ取得後にローディングが消える', async () => {
  render(<DataComponent />);
  
  // fetchDataの完了を確実に待つ
  await act(async () => {
    await fetchData();
  });
  
  // この時点でfetchDataは確実に完了している
  expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
});

2. 複数のactが重複して実行される

なぜFlaky Testになる?

  • バッチ処理の混乱: Reactの状態更新バッチが正しく処理されない
  • 実行順序の不確定性: 複数の非同期処理の完了順序が毎回変わる可能性

悪い例

test('複数のアクションを順次実行', async () => {
  render(<Counter />);
  
  // 前のactが完了していない状態で次のactが実行される
  act(async () => { 
    fireEvent.click(screen.getByText('Increment')); 
  });
  act(async () => { 
    fireEvent.click(screen.getByText('Increment')); 
  });
  
  // カウンターの値が1なのか2なのか不確定
  expect(screen.getByText('Count: 2')).toBeInTheDocument();
});

良い例

逐次実行な形に改善する。

test('複数のアクションを順次実行', async () => {
  render(<Counter />);
  
  // 最初のactの完了を待つ
  await act(async () => { 
    fireEvent.click(screen.getByText('Increment')); 
  });
  
  // 次のactを実行
  await act(async () => { 
    fireEvent.click(screen.getByText('Increment')); 
  });
  
  // この時点でカウンターは確実に2
  expect(screen.getByText('Count: 2')).toBeInTheDocument();
});

3. UIの最新状態を見ていない

なぜFlaky Testになる?

  • レンダリングの遅延: 状態更新からDOM反映までにタイムラグがある
  • useEffectの実行タイミング: 副作用の実行タイミングが予測できない
  • ブラウザの描画サイクル: ブラウザの描画処理が完了していない状態でテストが実行される

悪い例

test('ボタンクリック後にメッセージが表示される', () => {
  render(<MessageComponent />);
  
  // ボタンをクリック
  fireEvent.click(screen.getByText('Show Message'));
  
  // 状態更新がまだDOMに反映されていない可能性
  expect(screen.getByText('Hello World!')).toBeInTheDocument();
});

良い例

waitForを利用する。

test('ボタンクリック後にメッセージが表示される', async () => {
  render(<MessageComponent />);
  
  fireEvent.click(screen.getByText('Show Message'));
  
  // メッセージの表示を待つ
  await waitFor(() => {
    expect(screen.getByText('Hello World!')).toBeInTheDocument();
  });
});

actよりもwaitForfindByを推奨する理由

  • 可読性が高い
    waitForfindByはテストコードの意図が明確になり、何を待っているのかが一目で理解できる
  • バグを防ぎやすい
    非同期の状態更新や描画を自動的に待つため、actのawait漏れや重複呼び出しによるFlaky Testを防ぎやすくなる
  • テストの保守性が高まる
    UIの変更や副作用のタイミングが変わっても、waitForfindByを使っていればテストを壊しにくい

まとめ

  • actのWarningは「テストがReactの状態更新を正しく待てていない」サイン
  • act(async () => {...})には必ずawaitを付けることが重要
  • 可能な限りTesting LibraryのwaitForfindByを使用する
  • Flaky Testの多くは非同期処理の待ち方のミスが原因

参考文献

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?