はじめに
このプロジェクトでは、React + Jest + Testing Libraryを使用して学習記録アプリのテストを実装しています。この記事では、実際のプロジェクトの設定とテストコードを基に、テスト環境の構築方法とテストの書き方を説明します。
プロジェクト概要
学習記録アプリは、Supabaseをバックエンドとして使用するReactアプリケーションです。ユーザーは学習内容と学習時間を入力し、記録を管理できます。
テスト環境のセットアップ
必要なパッケージ
package.jsonに以下のパッケージが含まれています:
-
jest: テストフレームワーク -
jest-environment-jsdom: ブラウザ環境のシミュレート -
@testing-library/react: Reactコンポーネントのテスト -
@testing-library/jest-dom: DOM要素のマッチャー -
babel-jest: Babelを使用したJavaScript変換 -
identity-obj-proxy: CSSファイルのモック
設定ファイル
jest.config.mjs
export default {
testEnvironment: "jsdom",
moduleNameMapper: {
"\\.(css|less)$": "identity-obj-proxy",
},
setupFilesAfterEnv: ["./jest.setup.js"],
};
-
testEnvironment: "jsdom": ブラウザ環境をシミュレート -
moduleNameMapper: CSSファイルをモック化 -
setupFilesAfterEnv: テスト実行前に読み込む設定ファイル
jest.setup.js
import "@testing-library/jest-dom";
// dotenvの設定
require("dotenv").config();
-
@testing-library/jest-domのマッチャーを有効化 - 環境変数の読み込み
.babelrc
{
"presets": [
"@babel/preset-env",
[
"@babel/preset-react",
{
"runtime": "automatic"
}
]
]
}
- ES6+の構文とJSXを変換
モック(Mock)とは?なぜモックを使うのか?
モックとは
モックとは、テスト時に実際のオブジェクトや外部サービスの代わりに使用する「偽物」のオブジェクトです。本物と同じインターフェース(メソッドや戻り値の形式)を持ちながら、テスト用に制御された振る舞いをします。
DBに接続しなくても模擬でDBを操作しているように振る舞う振る舞いを定義する
なぜモックが必要なのか
このプロジェクトでは、Supabase(データベースサービス)をモック化しています。モックを使う理由は以下の通りです:
1. テストの独立性を保つ
実際のSupabaseに接続すると、テストの結果がデータベースの状態に依存してしまいます。例えば:
- 他の開発者がデータを変更したら、テストが失敗するかもしれない
- テスト前後でデータベースの状態が変わってしまう
モックを使えば、外部サービスの状態に関係なく、常に同じ条件でテストを実行できます。
2. テストを高速に実行する
実際のデータベースへの接続には、ネットワーク通信が発生します。これには時間がかかります。モックを使えば、ネットワーク通信なしで即座にデータを返すため、テストが高速に実行されます。
3. テスト結果を安定させる(再現性)
モックは毎回同じデータを返すように設定できます。これにより:
- 今日実行しても、明日実行しても、同じ結果が得られる
- CI/CD環境でも、ローカル環境でも、同じテスト結果になる
テストの再現性は、信頼できるテストを書く上で非常に重要です。
4. 様々なケースをシミュレートできる
モックを使えば、実際には発生させにくい状況を簡単にテストできます:
- データベース接続エラー
- 空のデータが返ってくる場合
- 大量のデータが返ってくる場合
// エラーケースのモック例
select: jest.fn(() => Promise.resolve({ data: null, error: { message: "接続エラー" } }))
5. コストと環境の問題を回避
実際のAPIを呼び出すと:
- API利用料金が発生する可能性がある
- テスト環境用のデータベースを用意する必要がある
- 環境変数やクレデンシャルの設定が必要
モックを使えば、追加のコストや環境設定なしでテストを実行できます。
モックの考え方
モックを作る際のポイントは、「テスト対象が期待する振る舞いを再現する」ことです。
このプロジェクトでは、AppコンポーネントがSupabaseの以下のメソッドを使用しています:
-
supabase.from("study-record").select()- データの取得 -
supabase.from("study-record").insert().select()- データの挿入 -
supabase.from("study-record").delete().eq()- データの削除
したがって、モックでもこの構造(メソッドチェーン)を再現し、期待されるデータ形式を返すように設定します。
テストコードの実装
実際のテストファイル: src/test/sample.spec.js
import App from "../index.jsx";
import React from "react";
import '@testing-library/jest-dom'
import { render, screen } from "@testing-library/react";
jest.mock("../supabase.js", () => {
const mockRecords = [
{
id: 1,
title: "test",
time: 3,
}
];
return {
__esModule: true,
default: {
from: jest.fn(() => ({
select: jest.fn(() => Promise.resolve({ data: mockRecords, error: null })),
insert: jest.fn(() => ({
select: jest.fn(() => Promise.resolve({ data: mockRecords, error: null }))
})),
delete: jest.fn(() => ({
eq: jest.fn(() => Promise.resolve({ error: null }))
}))
}))
}
};
});
describe("学習記録アプリ", () => {
it("タイトルが表示されていること", async () => {
render(<App />);
const title = await screen.findByTestId("title");
expect(title).toHaveTextContent("test");
});
});
テストの説明
-
Supabaseのモック:
jest.mock()でSupabaseクライアントをモック化し、実際のデータベース接続なしでテストを実行 -
コンポーネントのレンダリング:
render(<App />)でアプリをレンダリング -
要素の取得:
screen.findByTestId("title")でdata-testid="title"を持つ要素を取得(非同期で待機) -
アサーション:
expect(title).toHaveTextContent("test")でテキスト内容を確認
Supabaseモックのポイント
Supabaseはメソッドチェーン(from().select()など)を使用するため、モックでも同じ構造を再現します:
-
from(): テーブルを指定 -
select(): データ取得 -
insert().select(): データ挿入 -
delete().eq(): データ削除
各メソッドはPromise.resolve()で非同期処理をシミュレートします。
テストの実行
npm test
テストを実行すると、src/test/ディレクトリ内のテストファイルが実行されます。
テストの拡張例
現在のテストは「タイトルが表示されていること」のみをテストしていますが、以下のようなテストを追加できます:
- 学習記録の一覧表示
- 学習記録の登録機能
- 学習記録の削除機能
- バリデーションエラーの表示
- 学習時間の合計表示
まとめ
このプロジェクトでは、JestとReact Testing Libraryを使用して学習記録アプリのテストを実装しています。Supabaseのような外部依存はモック化することで、テストを独立させ、高速に実行できるようにしています。
テストを追加・改善することで、コードの品質を保ち、リファクタリング時の安全性を確保できます。
大切なことなのでもう一度いいます。
テストでモックを使う理由は「DBに接続しなくても模擬でDBを操作しているように振る舞う振る舞いを定義する」
以上。