🐙

Jest × Testing Library 初心者が必ずハマる7つのエラーと解決法

に公開

はじめに

初めての投稿です!

みなさん、テストコード書いていますか?

私は4年目のフロントエンドエンジニアですが、今まで幾つかのプロジェクトを経験してきた中で、全てのプロジェクトで「テストコードは時間が余ったらやろっかー」と言って、結局やることはなく終わっていきました...笑 (同じ経験ありますかね?)

ついにその時がきた。

ついに現在のプロジェクトでテストコードを実装することになりました。ただ、チームメンバーもテストコードをガッツリ書いたことがないということで、私が代表としてキャッチアップすることになりました。

そこで実際にJestを触り始めたのですが...

あまり良い記事に巡り会えず、時間を溶かしてしまいました。

「なんでこのエラーが出るんだ?」「Stack Overflowに似たような質問はあるけど、微妙に状況が違う...」「公式ドキュメントを読んでもピンとこない...」

なんかちょうどいい実践向けの記事がなかなかなくて、超基礎の説明 or 公式ドキュメントに辿り着くんですよね〜
私は公式ドキュメントが苦手で、なんとかちょうど良いブログ記事はないかと色々調べましたが、結局は公式ドキュメントを見ることになりました。。。

気づけば半日が過ぎ、テストコード1つ書けていない。そんな経験を何度もしました。

この記事では、私が実際に遭遇して「うわあああ」となったエラー7選と、その解決法を初心者の頃の自分に教えるつもりで解説します。同じ苦労をする人が一人でも減れば嬉しいです。

エラー1:「toBeInTheDocument is not a function」

実際のエラーメッセージ

TypeError: expect(...).toBeInTheDocument is not a function

発生する状況

いざテストを書き始めて、要素の存在確認をしようとしたら出るエラー。私は最初の1時間でこれにハマりました。

// Button.test.js
import { render, screen } from '@testing-library/react';
import Button from '@/components/Button';

test('ボタンが表示される', () => {
  render(<Button>クリック</Button>);
  expect(screen.getByRole('button')).toBeInTheDocument(); // エラー!
});

解決方法

そもそも@testing-library/jest-domをインポートする必要があることを知りませんでした。

解決法1: jest.setup.tsでグローバル設定(推奨) ※ファイル名は任意でっせー

// src/jest.setup.ts
import '@testing-library/jest-dom';
// jest.config.ts
module.exports = {
  setupFilesAfterEnv: ['<rootDir>/jest.setup.ts']
};

解決法2: 各テストファイルでインポート

// Button.test.ts
import '@testing-library/jest-dom';
import { render, screen } from '@testing-library/react';
import Button from '@/components/Button';

test('ボタンが表示される', () => {
  render(<Button>クリック</Button>);
  expect(screen.getByRole('button')).toBeInTheDocument(); // OK!
});

なぜそうなるか

toBeInTheDocument()toHaveClass()などのDOM関連のマッチャー(検証関数)は、Jestデフォルトではなく、@testing-library/jest-domパッケージが提供する拡張機能です。このインポートを忘れると、これらの便利な関数が使えません。

予防策

  • プロジェクト開始時に必ずjest.setup.tsを設定する

エラー2:「要素が存在しないことをテストできない」

実際のエラーメッセージ

Unable to find an element with the text: TEST-001. This could be because the text is broken up by multiple elements.

発生する状況

「ログインしていない状態ではログインボタンが表示されない」ことをテストしようとしたとき。getByを使って存在しない要素を探そうとしてエラーになりました。

// 問題のあるコード
test('ログアウト状態ではログインボタンが表示されない', () => {
  render(<Header isLoggedIn={false} />);
  
  // 存在しない要素を getBy で探すとエラー!
  expect(screen.getByText('ログインボタン')).not.toBeInTheDocument(); // エラー!
});

解決方法

存在しないことを確認するにはqueryByを使う必要があります。

解決法1: queryByを使用

test('ログアウト状態ではログインボタンが表示されない', () => {
  render(<Header isLoggedIn={false} />);
  
  // queryBy は見つからなくても null を返す(エラーにならない)
  expect(screen.queryByText('ログインボタン')).not.toBeInTheDocument(); // OK!
});

解決法2: 条件分岐でテスト

test('ログイン状態によってボタンが切り替わる', () => {
  // ログアウト状態
  const { rerender } = render(<Header isLoggedIn={false} />);
  expect(screen.queryByText('ログアウト')).not.toBeInTheDocument();
  expect(screen.getByText('ログイン')).toBeInTheDocument();
  
  // ログイン状態に変更
  rerender(<Header isLoggedIn={true} />);
  expect(screen.queryByText('ログイン')).not.toBeInTheDocument();
  expect(screen.getByText('ログアウト')).toBeInTheDocument();
});

なぜそうなるか

Testing Libraryのクエリには3つの種類があります:

  • getBy:要素を見つける。見つからなければエラーを投げる
  • queryBy:要素を見つける。見つからなければnullを返す
  • findBy:要素が現れるまで待つ。見つからなければエラー

存在しないことを確認したい場合は、エラーにならないqueryByを使う必要があります。

getByだとそんなもんねぇよ!!って怒られちゃうんだね😢

予防策

  • 「存在する」→ getBy または findBy
  • 「存在しない」→ queryBy
  • クエリの使い分けを覚える

この三つのクエリはマジでなんとなくで使わないように!

エラー3:「act()」警告 ★最重要★

実際のエラーメッセージ

Warning: An update to TestComponent inside a test was not wrapped in act(...).

発生する状況

これめっちゃ出た。。。
しかもなんか大量に出てきやがt...

頭冷やしてきました。
React コンポーネントのテストで状態更新をテストしようとしたとき。これが一番頻出で、一番時間を取られました

// 問題のあるコード
test('ボタンクリックでカウントが増える', () => {
  render(<Counter />);
  const button = screen.getByRole('button');
  
  fireEvent.click(button); // 警告が出る!
  
  expect(screen.getByText('1')).toBeInTheDocument();
});

解決方法

解決法1: actで手動ラップ

import { act } from '@testing-library/react';

test('ボタンクリックでカウントが増える', () => {
  render(<Counter />);
  const button = screen.getByRole('button');
  
  act(() => {
    fireEvent.click(button);
  });
  
  expect(screen.getByText('1')).toBeInTheDocument();
});

解決法2: userEventを使う(推奨)

import userEvent from '@testing-library/user-event';

test('ボタンクリックでカウントが増える', async () => {
  const user = userEvent.setup();
  render(<Counter />);
  const button = screen.getByRole('button');
  
  await user.click(button); // これで解決!
  
  expect(screen.getByText('1')).toBeInTheDocument();
});

これでもダメな時は次に紹介するwaitForを使って!

なぜそうなるか(詳細解説)

これを理解するために、Reactの仕組みを知る必要があります。

Reactの状態更新は非同期的に処理されます。具体的には:

  1. setStateが呼ばれる
  2. Reactが「状態更新をスケジュール」する
  3. 次のイベントループで実際に更新を実行
  4. コンポーネントが再レンダリングされる
// Counter.jsx の例
function Counter() {
  const [count, setCount] = useState(0);
  
  const handleClick = () => {
    setCount(count + 1); // これは非同期!
    // ここではまだcountは0のまま
  };
  
  return (
    <div>
      <span>{count}</span>
      <button onClick={handleClick}>+1</button>
    </div>
  );
}

fireEvent.click()同期的にイベントを発火させます。しかし、その結果の状態更新は非同期的に処理されます。

// 問題のあるテスト
fireEvent.click(button);
// ↑ この時点では、まだsetStateはスケジュールされただけ
expect(screen.getByText('1')).toBeInTheDocument();
// ↑ 状態更新が完了する前にテストが実行される

act()の役割は「この中で起きる全ての状態更新を完了させてから次に進む」ことです:

act(() => {
  fireEvent.click(button);
}); // ← ここで状態更新が完全に完了するまで待つ
expect(screen.getByText('1')).toBeInTheDocument(); // ← 安全にテスト実行

予防策

  • fireEventよりuserEventを使う(内部で適切に非同期処理される)
  • 状態更新を伴うテストでは必ずact()を意識する
  • 「なんで警告が出るんだろう?」ではなく「Reactの更新サイクルを理解する」

エラー4:「findByRole」のタイムアウト

実際のエラーメッセージ

Unable to find an accessible element with the role "button"

発生する状況

非同期でレンダリングされる要素を待とうとしたとき。

// 問題のあるコード
test('ローディング後にボタンが表示される', async () => {
  render(<AsyncComponent />);
  
  // すぐに探しても見つからない
  const button = screen.getByRole('button'); // エラー!
  expect(button).toBeInTheDocument();
});

解決方法

解決法1: findByを使う

test('ローディング後にボタンが表示される', async () => {
  render(<AsyncComponent />);
  
  // 要素が現れるまで待つ
  const button = await screen.findByRole('button');
  expect(button).toBeInTheDocument();
});

解決法2: waitForを使う

import { waitFor } from '@testing-library/react';

test('ローディング後にボタンが表示される', async () => {
  render(<AsyncComponent />);
  
  await waitFor(() => {
    expect(screen.getByRole('button')).toBeInTheDocument();
  });
});

なぜそうなるか

何度説明しても良い もっかい見とこうか!

  • getBy:今すぐ要素を探す(見つからなければ即エラー)
  • findBy:要素が現れるまで待つ(デフォルト1秒)
  • queryBy:見つからなくてもエラーにならない(nullを返す)

予防策

非同期処理があるテストではfindByを使う

エラー5:「jest.mock()」の使い方

マジでモックの概念さえ知らなくて時間溶けた。。。

実際のエラーメッセージ

TypeError: Cannot read property 'mockReturnValue' of undefined

発生する状況

外部APIをモックしようとしたとき。

// 問題のあるコード
import { fetchUser } from '@/api/user';

jest.mock('@/api/user');

test('ユーザー情報が表示される', async () => {
  fetchUser.mockReturnValue({ name: 'テスト太郎' }); // エラー!
});

解決方法

解決法1: jest.Mockで明示的にモック

import * as userApi from '@/api/user';

jest.mock('@/api/user');
const mockedUserApi = userApi as jest.Mock; //←こいつね

test('ユーザー情報が表示される', async () => {
  mockedUserApi.fetchUser.mockResolvedValue({ name: 'テスト太郎' });
  
  render(<UserProfile />);
  expect(await screen.findByText('テスト太郎')).toBeInTheDocument();
});

なぜそうなるか

Jestはjest.mock()でモジュール全体を置き換えますが、TypeScriptの型情報が失われるため、明示的に型を指定する必要があります。

予防策

モック関数はas jest.Mockで必ず明示的にモックであることを指定する

エラー6:「モックが期待通りに動かない」

実際のエラーメッセージ

Expected number of calls: 0
Received number of calls: 1

発生する状況

前のテストのモックの影響が残っているとき。

// 問題のあるコード
const mockFn = jest.fn();

describe('テストスイート', () => {
  test('最初のテスト', () => {
    mockFn();
    expect(mockFn).toHaveBeenCalled();
  });

  test('2番目のテスト', () => {
    // 前のテストの呼び出し履歴が残っている!
    expect(mockFn).toHaveBeenCalledTimes(0); // エラー!
  });
});

解決方法

解決法1: beforeEachでクリア

const mockFn = jest.fn();

describe('テストスイート', () => {
  beforeEach(() => {
    jest.clearAllMocks(); // 履歴をクリア
  });

  test('最初のテスト', () => {
    mockFn();
    expect(mockFn).toHaveBeenCalled();
  });

  test('2番目のテスト', () => {
    expect(mockFn).toHaveBeenCalledTimes(0); // OK!
  });
});

なぜそうなるか

モック関数は呼び出し履歴を保持します。テスト間でクリアしないと、前のテストの影響が残ります。

予防策

  • beforeEachで必ずモックをクリアする

エラー7:「非同期処理のテストが不安定」

内容被ってるかもしれないけど、改めて

実際のエラーメッセージ

expect(received).toBeInTheDocument()
Expected: <div>Loading...</div>
Received: null

発生する状況

非同期処理の完了を待たずにテストが実行されるとき。

// 問題のあるコード
test('データ取得後に内容が表示される', () => {
  render(<AsyncDataComponent />);
  
  // 非同期処理が完了する前にテストが実行される
  expect(screen.getByText('データ内容')).toBeInTheDocument(); // エラー!
});

解決方法

解決法1: findByでデータ出現を待つ

test('データ取得後に内容が表示される', async () => {
  render(<AsyncDataComponent />);
  
  // データが出現するまで待つ
  expect(await screen.findByText('データ内容')).toBeInTheDocument();
});

解決法2: ローディング状態もテストする

test('データ取得の流れをテストする', async () => {
  render(<AsyncDataComponent />);
  
  // 最初はローディング表示
  expect(screen.getByText('Loading...')).toBeInTheDocument();
  
  // データが表示されるまで待つ
  expect(await screen.findByText('データ内容')).toBeInTheDocument();
  
  // ローディングが消える
  expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
});

なぜそうなるか

非同期処理(API呼び出しなど)は時間がかかります。その完了を待たずにテストが実行されると、期待する要素がまだ表示されていません。

予防策

  • 非同期処理があるコンポーネントでは必ずawaitを使う
  • ローディング状態も含めてテストする

まとめ

4年目エンジニアとして、テストコードは意外と参考になる記事が少ないし、後回しにしがちと実感しています。しかし、これらのエラーを一度経験すれば、次からは迷わずに対処できるようになります。

今回のエラー解決で学んだこと

  1. パッケージのバージョンを意識する:古い記事の情報に惑わされない
  2. Reactの更新サイクルを理解するact()警告の根本原因を知る
  3. 非同期処理はawaitを忘れずにfindBy系の使い分けを覚える
  4. モックは型とクリアを意識する:テスト間の影響を防ぐ

チームでテストを導入する際のコツ

  • 小さく始める:いきなり全部テストしようとしない
  • エラーを恐れない:みんな通る道だと割り切る
  • 頻出エラーの内容を共有する:はまりポイントとかをドキュメントにまとめて共有しておくと同じエラーに苦しむ人が減る→生産性が上がる!

「テスト書いた方がいいのは分かるけど...」から「テスト書くと安心!」に変わる日が必ず来ます。

私もテストを実施したことで、機能追加をしていくたびに思わぬバグに気づかせてもらえるようになりました。また、影響範囲の大きい修正もテストがあると安心できます🤤

最初は時間がかかりますが、バグを事前に発見できる安心感リファクタリング時の心理的安全性は何物にも代えがたいものです。

一緒に頑張りましょう!


参考リンク

この記事がJest学習で詰まっている方の助けになれば幸いです。

好評であれば、以下のような記事も書いていこうと思います:

  • Testing Libraryクエリの優先順位(getByRole vs getByTestId など)
  • テストカバレッジの設定と活用法
  • Next.js + TypeScript でのテスト環境構築
  • AI活用したテストコード自動生成

ぜひいいね👍やコメントで反応をお聞かせください!

質問やコメントがあれば、お気軽にどうぞ!

Discussion