Reactユニットテスト|Vite + React + TypeScript + Jest + React Testing Library の構成でReactユニットテストを学んだ
React + TypeScripの開発環境でユニットテストを書いてみる
5/29/2025
開発環境の構築(Vite + Jest + RTL)
Vite + Jest + RTL(React Testing Library)で環境を構築する。
🔸①初期セットアップ
vite最新verとテンプレートを使用してディレクトリを作成し、npmパッケージマネージャーをプロジェクトへインストール。
npm create vite@latest my-testing-app -- --template react-ts
cd my-testing-app
npm install🔸②JestでTypeScript/Reactコンポーネントをテストできる環境を作成
npm install --save-dev jest @types/jest jest-environment-jsdom babel-jest @testing-library/react @testing-library/jest-dom @testing-library/user-event @babel/preset-env @babel/preset-react @babel/preset-typescript✅インストールしたパッケージの内容を解説
パッケージ名 | 役割 |
|---|---|
jest | テストランナー本体 |
@types/jest | TypeScriptでJestの型定義を使うため |
jest-environment-jsdom | ブラウザ環境(DOM)を再現する仮想環境 |
babel-jest | Babelを使ってテスト用コードを変換するブリッジ |
@babel/preset-env | 最新JavaScriptを指定環境向けに変換 |
@babel/preset-react | JSXなどReact用の構文を変換 |
@babel/preset-typescript | TypeScriptコードをBabelで変換 |
@testing-library/react | ReactコンポーネントをテストするためのTesting Libraryの中核パッケージ |
@testing-library/jest-dom |
|
@testing-library/user-event | ユーザー操作(クリック、入力など)をシミュレート |
📝babel-jestの解説
前回jest×TypeScriptでテスト環境を整えたときに、babel-jestは入れずにts-jestをインストールしていた。
ts-jestはTypeScriptのstrictな型設定を忠実に反映したいときや、Node.js向けのTypeScriptバックエンドテスト(Reactなし)の場合に使用されるとのこと。babel-jest と ts-jest を併用するのは推奨されない。
トランスパイラ | 特徴 | 向いている場面 |
|---|---|---|
| JSXやESModulesに強いが型情報は無視 | フロントエンド(Vite + React)テスト |
| TypeScriptの設定を忠実に再現 | TypeScript全体管理したい or Node.js側など |
📝@testing-library/reactの解説
Reactのコンポーネントをテストする際、内部実装(クラス名やDOM構造)に依存せず、実際のユーザーがどう見るかを重視したテストが書けるようになる。(ユーザー視点で「画面上に何が見えているか」をテストするためのAPIを提供する。)
提供する主な機能(関数)
関数 | 説明 |
|---|---|
| コンポーネントを仮想DOMにレンダリング |
| 画面上に見えている要素を取得するためのクエリをまとめたオブジェクト |
| ユーザーが見るテキストやラベル、ロールをもとに要素を取得 |
| 非同期や存在しない要素への対応 |
🔸③babel.config.js の作成
プロジェクト内にbabel.config.jsファイルを作成。
module.exports = {
presets: [
"@babel/preset-env",
["@babel/preset-react", { runtime: "automatic" }],
"@babel/preset-typescript",
],
};🔸④setupTests.ts の作成 |src/setupTests.ts
プロジェクトのsrc/配下にsetupTests.tsファイルを作成。
import '@testing-library/jest-dom';🔸⑤jest.config.js の作成|jest.config.js
プロジェクト内にjest.config.jsファイルを作成。
export default {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'],
transform: {
'^.+\\.(ts|tsx)$': 'babel-jest',
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
};開発環境で詰まった部分は下記記事参照。
Reactテスト|Jest + Babel + TypeScript + React 開発環境での落とし穴
Reactのユニットテストとは
ユニットテスト(unit test)の「ユニット」は「最小単位」を意味する。
Reactコンポーネントで言えば:
一つのボタン
一つの入力フォーム
一つの関数
一つの状態管理ロジック
など、「小さな機能・部品単位」で分解して、それぞれが正しく動作するかを1つずつ確認する。
1つの部品が、こういう入力に対して正しく反応するかにフォーカス。責務(やるべきこと)を明確に絞り込んでいる。
テスト例1:ボタンのテスト
目的
- ボタンをクリックすると、クリックハンドラによって正しく関数が呼ばれるかを確認。
🔸Buttonコンポーネント作成|src/components/Button.tsx
type ButtonProps = {
onClick: () => void;
children: React.ReactNode;
}
export const Button: React.FC<ButtonProps> = ({ onClick , children }) => {
return <button onClick={onClick}>{children}</button>
}📝解説
型定義 type ButtonPropsでpropsの型を決める。onClickはクリック時に呼ばれる関数で、引数も戻り値もないシンプルな型。childrenはReact.ReactNode型で、文字列やReact要素などボタン内に入れられるもの全般を許容。
React.FC Reactの関数コンポーネントに型を付けるための型定義。React.FC<Props> のように使って、propsの型をコンポーネントに明示的に与えるのが目的。
下記と同じ
const Button = ({ onClick, children }: ButtonProps) => {
return <button onClick={onClick}>{children}</button>;
};🔸Buttonコンポーネントのテストを作成|src/components/tests/Button.test.tsx
import { fireEvent, render, screen } from '@testing-library/react';
import { Button } from "../Button";
test('クリック時にonClickが呼ばれる', () => {
const handleClick = jest.fn(); // モック関数を作成(何度呼ばれたかなどを検証できる)
render(<Button onClick={handleClick}>クリックして</Button>);
// 画面上のボタンを取得(ボタンのテキストが "クリックして" のもの)
const button = screen.getByRole('button', { name: /クリックして/i });
// クリックイベントを発火
fireEvent.click(button);
// handleClickが1回呼ばれていることを検証
expect(handleClick).toHaveBeenCalledTimes(1);
});📝解説
jest.fn()でモック関数handleClickを作り、クリック時にこの関数がちゃんと呼ばれるかをテスト。render()でコンポーネントを仮想DOMに描画。screen.getByRole('button', { name: /クリックして/i })
→「roleがbutton」で、かつラベル(テキスト)が「クリックして」にマッチする要素を取得。Buttonコンポーネントのbutton内の{Children}のテキストに対応している。
→screenは画面(DOM)に描画された要素へアクセスするためのオブジェクト。fireEvent.click(button)でクリック操作を模倣。- 最後に
expect(handleClick).toHaveBeenCalledTimes(1)で、クリック時にモック関数が1回だけ呼ばれたことを期待するテストコードを入れる。
🔸テスト結果
PASS src/compornents/__tests__/Button.test.tsx
✓ クリック時にonClickが呼ばれる (52 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 0.783 s上記のことから、ユーザー目線でボタンをクリックすると1回onClickの関数が呼ばれることがテストで分かった。
テスト例2:フォームのテスト
テストの目的
- ログインフォームに メールアドレス と パスワード を入力できるか確認。
- 入力後にボタンをクリックすると、送信ハンドラが正しく呼ばれるかを確認。
🔸LoginFormコンポーネント作成|src/components/LoginForm.tsx
import { useState } from "react";
type LoginFormProps = {
onSubmit:(data: { email: string, password: string }) => void;
};
export const LoginForm: React.FC<LoginFormProps> = ({ onSubmit }) => {
const [email, setEmail] = useState('');
const [password , setPassword] = useState('');
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
onSubmit({ email, password });
};
return (
<form onSubmit={handleSubmit}>
<label>
Email:
<input
type="email"
placeholder="Email"
value={email}
onChange={e => setEmail(e.target.value)}
/>
</label>
<label>
Password:
<input
type="password"
placeholder="Password"
value={password}
onChange={e => setPassword(e.target.value)}
/>
</label>
<button type="submit">ログイン</button>
</form>
);
};📝解説
onSubmitの型定義 引数:1つのオブジェクト({ email: string, password: string })で、戻り値を返さない(void)
🔸LoginFormコンポーネントのテストを作成|src/components/LoginForm.test.tsx
import { render, screen, fireEvent } from "@testing-library/react";
import { LoginForm } from "../LoginForm";
describe('ログインフォーム', () => {
test('入力値の変更とフォーム送信ができる', () => {
const mockSubmit = jest.fn();
render(<LoginForm onSubmit={mockSubmit} />); // モック関数(送信処理の確認用)
// 入力フィールド取得
const emailInput = screen.getByPlaceholderText('Email');
const passwordInput = screen.getByPlaceholderText('Password');
const submitButton = screen.getByRole('button', {name: /ログイン/i });
// 値を入力
fireEvent.change(emailInput, { target: { value: 'test@example.com' }});
fireEvent.change( passwordInput, { target: { value: 'password123' }});
// 入力された値の確認
expect((emailInput as HTMLInputElement).value).toBe('test@example.com');
expect((passwordInput as HTMLInputElement).value).toBe('password123');
// ボタンをクリック(送信)
fireEvent.click(submitButton);
// 送信ハンドラが正しく呼ばれたか確認
expect(mockSubmit).toHaveBeenCalledTimes(1);
expect(mockSubmit).toHaveBeenCalledWith({
email: 'test@example.com',
password: 'password123',
});
});
});📝解説
mockSubmit = jest.fn()でモック関数を作成。render()でコンポーネントを仮想DOMに描画。screen.getByPlaceholderText('Email')でプレイスフォルダーがEmailを取得。screen.getByPlaceholderText('Password')でプレイスフォルダーがPasswordを取得。screen.getByRole('button', {name: /ログイン/i })
→「roleがbutton」で、かつラベル(テキスト)が「ログイン」にマッチする要素を取得。
テスト例3:非同期通信のテスト
テストの目的
- 初期表示で「読み込み中...」が表示されるか データ取得前の状態をチェック
- fetchUser が呼び出されるか 正しく非同期関数が発火されているか
- ユーザー名が正しく表示されるか
🔸UserProfileコンポーネント作成|src/components/LoginForm.tsx
// UserProfile.tsx
import React, { useEffect, useState } from 'react';
type User = {
id: string;
name: string;
};
type UserProfileProps = {
userId: string;
fetchUser: (id: string) => Promise<User>;
};
export const UserProfile: React.FC<UserProfileProps> = ({ userId, fetchUser }) => {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId, fetchUser]);
if (!user) return <div>読み込み中...</div>;
return <div>{user.name}</div>;
};🔸UserProfileコンポーネントのテストを作成|src/components/UserProfile.test.tsx
// src/components/UserProfile.test.tsx
import { render, screen, waitFor } from '@testing-library/react';
import { UserProfile } from '../UserProfile';
describe('UserProfile', () => {
test('初期表示で「読み込み中...」が表示される', () => {
const mockFetchUser = jest.fn().mockResolvedValue({ id: '1', name: 'テストユーザー' });
render(<UserProfile userId="1" fetchUser={mockFetchUser} />);
// 初期表示中の確認
expect(screen.getByText(/読み込み中/)).toBeInTheDocument();
});
test('fetchUser が呼び出され、ユーザー名が表示される', async () => {
const mockFetchUser = jest.fn().mockResolvedValue({ id: '1', name: 'テストユーザー' });
render(<UserProfile userId="1" fetchUser={mockFetchUser} />);
// fetchUser が正しく呼ばれたかを確認
expect(mockFetchUser).toHaveBeenCalledWith('1');
expect(mockFetchUser).toHaveBeenCalledTimes(1);
// 非同期に名前が表示されるのを待つ
await waitFor(() => {
expect(screen.getByText('テストユーザー')).toBeInTheDocument();
});
});
});📝解説
① モック関数を用意する(仮の fetch 処理)
const mockFetchUser = jest.fn().mockResolvedValue({ id: '1', name: 'テストユーザー' });jest.fn()→ Jest のモック関数(呼び出し回数や引数を記録).mockResolvedValue(...)→ この関数が Promise を返し、値としてユーザーデータを返すようにする(非同期処理を再現)
② テスト対象のコンポーネントに props として渡す
render(<UserProfile userId="1" fetchUser={mockFetchUser} />);userId="1"→ fetch の引数として渡す IDfetchUser={mockFetchUser}→ 実際の API 呼び出しの代わりにモック関数を使用
③ 初回レンダリング時に「読み込み中...」が表示されるか確認
expect(screen.getByText(/読み込み中/)).toBeInTheDocument();- 初期状態では
user === nullのため、「読み込み中...」が表示される - 非同期処理が始まる前の状態をテスト
④ useEffect の処理を確認(モック関数が呼ばれたか)
expect(mockFetchUser).toHaveBeenCalledWith('1');fetchUser(userId)が呼ばれたかをチェック(中身は mockFetchUser)userId="1"が引数として渡されていることを検証
⑤ fetch 処理完了後、ユーザー名が DOM に表示されるのを待つ
await waitFor(() => { expect(screen.getByText('テストユーザー')).toBeInTheDocument(); });mockFetchUserが返すユーザーデータがuseStateに反映され、再描画が起きるuser.name(ここでは「テストユーザー」)が画面に表示されるかを確認waitForを使うことで、非同期的に状態が変化するのを待つ
✅ React Testing Library(RTL)の関数一覧
関数名 | 主な用途・目的 |
|---|---|
| コンポーネントを仮想 DOM にレンダリングする |
| 指定テキストを含む要素を即座に取得(見つからないとエラー) |
| 指定テキストの要素を非強制で取得(見つからなければ |
| 非同期に要素を取得(Promise で待てる) |
| 指定の状態になるまで待機してからコールバックを実行(DOM の変化を待つときに使う) |
| クリックなどのユーザー操作を手動でトリガーする |
| 実際のユーザー操作に近い動作(遅延やフォーカス制御あり) |
| テキスト入力をシミュレート(フォームの入力に使う) |
✅Jest の関数一覧(テストフレームワーク)
関数名・マッチャー | 主な用途・目的 |
|---|---|
| 値に対しての期待(アサーション)を設定 |
| 要素が DOM に存在しているかを検証( |
| モック関数が呼び出されたかを確認 |
| モック関数が指定の引数で呼び出されたか |
| モック関数が指定回数だけ呼ばれたか |
| モック関数を生成(引数・戻り値・回数などの記録が可能) |
| モジュール全体をモック化する |
| モック関数が Promise.resolve を返すように設定 |
| モック関数が Promise.reject を返すように設定 |