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?

【Next.js、Jest】環境構築〜実装〜テスト実行までの流れ

Posted at

0.はじめに

現在Claude Codeを利用してチャットアプリを作成しており、
Jestを使ってテストを行っております。

ただClaude Codeが作成したテストコードをちゃんと理解する必要があるなと思い、
本記事を作成しながら学習して理解を深めようと思った次第です。

1.Jestとは

Meta社(旧Facebook社)が開発した、
JavaScriptおよびTypeScriptアプリケーション向けのテストフレームワークです。

豊富な機能を内蔵し、シンプルな構文と設定不要の設計で、
ユニットテストの作成から実行、
コードカバレッジの計測までを容易に行うことができます。
特にReact、Node.js、Vue、Angular、Next.jsなどの
様々なJavaScriptライブラリやフレームワークに対応しており、
高速なテスト実行と詳細なエラーレポートが特徴です。

1.1 Jestの特徴(メリット)

シンプルな設計と導入の容易さ

  • 設定がほぼ不要で簡単に利用できる
  • 直感的で分かりやすいAPIを提供し、負担なくテストコードを書ける

豊富な機能

  • モック機能:
    テスト対象のコードが依存する外部機能やライブラリを模倣し、
    テストの実行を独立して行える。
  • スナップショットテスト:
    大規模なオブジェクトの出力を記録し、次回以降の出力と自動で比較することで、
    予期せぬ変更を検出できます。
  • コードカバレッジ:
    テストコードによって、
    実際のコードがカバーされているかを計測する機能も内蔵。
  • テストの並列実行:
    テストを並列で実行することで、テストの実行時間を短縮し、
    開発の効率を高める。

高い互換性

  • JavaScriptおよびTypeScriptで書かれたあらゆるコードのテストをサポート
  • React、Vue、Angular、Node.js(ExpressやNestなど)といった
    様々なライブラリやフレームワークと統合して利用可能

詳細なエラーメッセージ

テストが失敗した際に、問題の原因を明確に示す
詳細なエラーメッセージが表示されるため、問題の特定と解決を容易に行える。

Next.jsでJestを使うメリット

  • Next.jsの機能(画像・スタイルのimportなど)を考慮した設定を
    next/jest経由で簡単に行える。

※公式ガイドでも Next.js + Jest + React Testing Library の組合せを推奨。

  • 組み込みのモック機能(jest.fn()など)で外部依存を切り離してテスト可能
  • DOM向けの便利な拡張マッチャー(@testing-library/jest-dom)を併用すると、
    読みやすいテストが書ける

自分のイメージですが、

  • 単体テスト(ユニットテスト)・結合テスト(インテグレーションテスト):Jest
  • 総合テスト(e2eテスト):PlayWright
    のイメージです。

2.環境構築

前提:Node.js(LTS推奨)がインストールされていること。

2.1 Next.js環境構築

npx create-next-app@latest my-app --yes
# --yes
# --yes を指定すると、保存された設定またはデフォルト設定を使用して
# プロンプトをスキップします。デフォルトの設定では、
# TypeScript、Tailwind、App Router、Turbopack が有効になり、
# インポートエイリアスは @/* になります。

# Jest本体 + Next.js対応
npm install -D jest jest-environment-jsdom @types/jest ts-jest ts-node
npm install -D @testing-library/react @testing-library/dom @testing-library/jest-dom @testing-library/user-event

// Jest初期設定
npm init jest@latest

 It seems that you already have a jest configuration, do you want to override it? … yes

The following questions will help Jest to create a suitable configuration for your project

✔ Would you like to use Typescript for the configuration file? … yes
✔ Choose the test environment that will be used for testing › jsdom (browser-like)
✔ Do you want Jest to add coverage reports? … yes
✔ Which provider should be used to instrument code for coverage? › v8
✔ Automatically clear mock calls, instances, contexts and results before every test? … yes
# 本記事ではデフォルト選択
# 上記選択を終えると、jest.config.tsが作成されます。

2.2 jest.config.ts(Jestの全体的な設定ファイル)

/**
 * For a detailed explanation regarding each configuration property, visit:
 * https://jestjs.io/docs/configuration
 */

import type { Config } from 'jest';
import nextJest from 'next/jest.js';

// Next.js 用の Jest 設定を生成するヘルパーを作る
const createJestConfig = nextJest({
    dir: './',  // Next.js プロジェクトのルートパス
})

const config: Config = {
    // All imported modules in your tests should be mocked automatically
    // automock: false,

    // Stop running tests after `n` failures
    // bail: 0,

    // The directory where Jest should store its cached dependency information
    // cacheDirectory: "/private/var/folders/0x/5tg412kd4cg3g7nm02wfhnd00000gn/T/jest_dx",

    // Automatically clear mock calls, instances, contexts and results before every test
    clearMocks: true,

    // Indicates whether the coverage information should be collected while executing the test
    collectCoverage: true,

    // An array of glob patterns indicating a set of files for which coverage information should be collected
    // collectCoverageFrom: undefined,

    // The directory where Jest should output its coverage files
    coverageDirectory: "coverage",

    // An array of regexp pattern strings used to skip coverage collection
    // coveragePathIgnorePatterns: [
    //   "/node_modules/"
    // ],

    // Indicates which provider should be used to instrument code for coverage
    coverageProvider: "v8",

    // A list of reporter names that Jest uses when writing coverage reports
    // coverageReporters: [
    //   "json",
    //   "text",
    //   "lcov",
    //   "clover"
    // ],

    // An object that configures minimum threshold enforcement for coverage results
    // coverageThreshold: undefined,

    // A path to a custom dependency extractor
    // dependencyExtractor: undefined,

    // Make calling deprecated APIs throw helpful error messages
    // errorOnDeprecated: false,

    // The default configuration for fake timers
    // fakeTimers: {
    //   "enableGlobally": false
    // },

    // Force coverage collection from ignored files using an array of glob patterns
    // forceCoverageMatch: [],

    // A path to a module which exports an async function that is triggered once before all test suites
    // globalSetup: undefined,

    // A path to a module which exports an async function that is triggered once after all test suites
    // globalTeardown: undefined,

    // A set of global variables that need to be available in all test environments
    // globals: {},

    // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
    // maxWorkers: "50%",

    // An array of directory names to be searched recursively up from the requiring module's location
    // moduleDirectories: [
    //   "node_modules"
    // ],

    // An array of file extensions your modules use
    // moduleFileExtensions: [
    //   "js",
    //   "mjs",
    //   "cjs",
    //   "jsx",
    //   "ts",
    //   "mts",
    //   "cts",
    //   "tsx",
    //   "json",
    //   "node"
    // ],

    // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
    // moduleNameMapper: {},

    // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
    // modulePathIgnorePatterns: [],

    // Activates notifications for test results
    // notify: false,

    // An enum that specifies notification mode. Requires { notify: true }
    // notifyMode: "failure-change",

    // A preset that is used as a base for Jest's configuration
    // preset: undefined,

    // Run tests from one or more projects
    // projects: undefined,

    // Use this configuration option to add custom reporters to Jest
    // reporters: undefined,

    // Automatically reset mock state before every test
    // resetMocks: false,

    // Reset the module registry before running each individual test
    // resetModules: false,

    // A path to a custom resolver
    // resolver: undefined,

    // Automatically restore mock state and implementation before every test
    // restoreMocks: false,

    // The root directory that Jest should scan for tests and modules within
    // rootDir: undefined,

    // A list of paths to directories that Jest should use to search for files in
    // roots: [
    //   "<rootDir>"
    // ],

    // Allows you to use a custom runner instead of Jest's default test runner
    // runner: "jest-runner",

    // The paths to modules that run some code to configure or set up the testing environment before each test
    // setupFiles: [],

    // A list of paths to modules that run some code to configure or set up the testing framework before each test
    // 各テスト実行前に読み込むファイルを指定
    // ここに書いたファイルの中身は、全テストで自動的に読み込まれる
    setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],

    // The number of seconds after which a test is considered as slow and reported as such in the results.
    // slowTestThreshold: 5,

    // A list of paths to snapshot serializer modules Jest should use for snapshot testing
    // snapshotSerializers: [],

    // The test environment that will be used for testing
    // テストを実行する環境を指定
    // jsdom = ブラウザっぽい環境
    testEnvironment: "jsdom",

    // Options that will be passed to the testEnvironment
    // testEnvironmentOptions: {},

    // Adds a location field to test results
    // testLocationInResults: false,

    // The glob patterns Jest uses to detect test files
    // testMatch: [
    //   "**/__tests__/**/*.?([mc])[jt]s?(x)",
    //   "**/?(*.)+(spec|test).?([mc])[jt]s?(x)"
    // ],

    // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
    // testPathIgnorePatterns: [
    //   "/node_modules/"
    // ],

    // The regexp pattern or array of patterns that Jest uses to detect test files
    // testRegex: [],

    // This option allows the use of a custom results processor
    // testResultsProcessor: undefined,

    // This option allows use of a custom test runner
    // testRunner: "jest-circus/runner",

    // A map from regular expressions to paths to transformers
    // transform: undefined,

    // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
    // transformIgnorePatterns: [
    //   "/node_modules/",
    //   "\\.pnp\\.[^\\/]+$"
    // ],

    // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
    // unmockedModulePathPatterns: undefined,

    // Indicates whether each individual test should be reported during the run
    // verbose: undefined,

    // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
    // watchPathIgnorePatterns: [],

    // Whether to use watchman for file crawling
    // watchman: true,
};

// 最終的に Jest が読む設定をエクスポート
export default createJestConfig(config);

2.2 jest.setup.ts(Jestの全テスト共通の初期設定ファイル)

// @testing-library/jest-dom を全テストで有効にする
import '@testing-library/jest-dom';

// --- 共通で使う fetch モックを定義 ---
beforeAll(() => {
    global.fetch = jest.fn(() =>
        Promise.resolve({
            ok: true,
            json: () => Promise.resolve({ message: 'ok' }),
        })
    ) as jest.Mock
});

afterAll(() => {
    // 各テスト後に呼び出し回数や引数をリセット
    jest.clearAllMocks();
})

2.3 package.json(Jestのコマンド追加)

"scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage"
}

--watch:開発中に変更を監視しながらテストを自動で再実行
--coverage:どのコードがテストでカバーされているかを確認

3.本実装・テストコード実装

3.1 本実装(サインアップ画面(react-hook-form使用))

"use client"

import { useState } from "react";
import { useForm } from "react-hook-form";

type SignUpFormInputs = {
    email: string;
    password: string;
}

type Props = {
    onSubmit?: (email: string, password: string) => void;
};

export default function SignUpPage({ onSubmit }: Props) {
    const [message, setMessage] = useState("");
    const { register, handleSubmit, formState: { errors, isSubmitting }, }
        = useForm<SignUpFormInputs>()

    // フォーム送信時の処理
    const submitHandler = async (data: SignUpFormInputs) => {
        if (onSubmit) {
            // テストから渡されたモック関数を呼び出す
            onSubmit(data.email, data.password);
            return
        }

        // API呼び出し(現状の実装だと動かない、テストはFetchモックでカバー)
        try {
            const res = await fetch('/api/signup', {
                method: 'POST',
                headers: { "Content-Type": "applications/json" },
                body: JSON.stringify(data),
            });

            const result = await res.json();
            if (res.ok) {
                setMessage(result.message || "サインアップ成功!");
            } else {
                setMessage(result.error || "サインアップに失敗しました");
            }
        } catch (err) {
            setMessage(`ネットワークエラーが発生しました:${err}`,)
        }
    }

    return (
        <div className="max-w-md mx-auto mt-10 p-6 border rounded">
            <h1 className="text-2xl font-bold mb-4">Sign Up</h1>
            <form
                onSubmit={handleSubmit(submitHandler)}
                className="space-y-4"
                noValidate
            >
                {/* メールアドレス */}
                <div>
                    <input
                        type="email"
                        placeholder="test@example.com"
                        className="border p-2 w-full"
                        {...register("email", {
                            required: "メールアドレスは必須です",
                            pattern: {
                                value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
                                message: "正しいメールアドレスを入力してください",
                            }
                        })}
                    />
                    {errors.email && (
                        <p className="text-red-500 text-sm">{errors.email.message}</p>
                    )}
                </div>
                {/* パスワード */}
                <div>
                    <input
                        type="password"
                        placeholder="********"
                        className="border p-2 w-full"
                        {...register("password", {
                            required: "パスワードは必須です",
                            minLength: {
                                value: 8,
                                message: "パスワードは8文字以上必要です",
                            }
                        })}
                    />
                    {errors.password && (
                        <p className="text-red-500 text-sm">{errors.password.message}</p>
                    )}
                </div>
                <button
                    type="submit"
                    disabled={isSubmitting}
                    className="bg-blue-500 text-white px-4 py-2 rounded"
                >
                    Sign Up
                </button>
            </form>
            {/* 結果メッセージ */}
            {message && <p className="mt-4">{message}</p>}
        </div>
    );
}

3.2 テストコード実装

src/app/__tests__にテストファイルを作成

import { render, screen } from '@testing-library/react';

import userEvent from '@testing-library/user-event'
import SignUpPage from '../signup/page';

describe('SignUpPage API 呼び出し', () => {
    it('サインアップ成功時に成功メッセージを表示する', async () => {
        const user = userEvent.setup();

        // fetchを成功レスポンスでモック
        ; (global.fetch as jest.Mock).mockResolvedValueOnce({
            ok: true,
            json: async () => ({ message: 'ユーザー作成されました!' }),
        })
        render(<SignUpPage />)

        // 入力操作
        await user.type(screen.getByPlaceholderText('test@example.com'), 'user@test.com');
        await user.type(screen.getByPlaceholderText('********'), 'secret123');

        // ボタンクリック
        await user.click(screen.getByRole('button', { name: /Sign Up/i }));

        // 成功メッセージが表示されることを確認
        expect(await screen.findByText("ユーザー作成されました!")).toBeInTheDocument();
    });

    it('サインアップ失敗時にエラーメッセージを表示する', async () => {
        const user = userEvent.setup();

        // fetch を失敗レスポンスでモック (res.ok = false)
        ; (global.fetch as jest.Mock).mockResolvedValueOnce({
            ok: false,
            json: async () => ({ error: 'サインアップに失敗しました' }),
        })

        render(<SignUpPage />)

        await user.type(screen.getByPlaceholderText('test@example.com'), 'user@test.com');
        await user.type(screen.getByPlaceholderText('********'), 'secret123');
        await user.click(screen.getByRole('button', { name: /Sign Up/i }));

        expect(await screen.findByText('サインアップに失敗しました')).toBeInTheDocument();
    });

    it('ネットワークエラー発生時にネットワークエラーメッセージを表示する', async () => {
        const user = userEvent.setup();

        // fetch を例外でモック
        ; (global.fetch as jest.Mock).mockRejectedValueOnce(new Error('timeout'))

        render(<SignUpPage />)

        await user.type(screen.getByPlaceholderText('test@example.com'), 'user@test.com');
        await user.type(screen.getByPlaceholderText('********'), 'secret123');
        await user.click(screen.getByRole('button', { name: /Sign Up/i }));

        expect(await screen.findByText(/ネットワークエラーが発生しました/)).toBeInTheDocument();
    });
});

describe('SignUpPage onSubmit prop', () => {
    it('onSubmit が渡された場合は API 呼び出しを行わず onSubmit を呼ぶ', async () => {
        const user = userEvent.setup();
        const mockSubmit = jest.fn();

        render(<SignUpPage onSubmit={mockSubmit} />);

        await user.type(screen.getByPlaceholderText('test@example.com'), 'user@test.com');
        await user.type(screen.getByPlaceholderText('********'), 'secret123');
        await user.click(screen.getByRole('button', { name: /Sign Up/i }));

        expect(mockSubmit).toHaveBeenCalledWith('user@test.com', 'secret123');
    });
});

describe('SignUpPage API fallback messages', () => {
    it('成功レスポンスに message がない場合はデフォルト成功メッセージを表示する', async () => {
        const user = userEvent.setup();

        ; (global.fetch as jest.Mock).mockResolvedValueOnce({
            ok: true,
            json: async () => ({}),
        })

        render(<SignUpPage />)

        await user.type(screen.getByPlaceholderText('test@example.com'), 'user@test.com');
        await user.type(screen.getByPlaceholderText('********'), 'secret123');
        await user.click(screen.getByRole('button', { name: /Sign Up/i }));

        expect(await screen.findByText('サインアップ成功!')).toBeInTheDocument();
    });

    it('失敗レスポンスに error がない場合はデフォルト失敗メッセージを表示する', async () => {
        const user = userEvent.setup();

        ; (global.fetch as jest.Mock).mockResolvedValueOnce({
            ok: false,
            json: async () => ({}),
        })

        render(<SignUpPage />)

        await user.type(screen.getByPlaceholderText('test@example.com'), 'user@test.com');
        await user.type(screen.getByPlaceholderText('********'), 'secret123');
        await user.click(screen.getByRole('button', { name: /Sign Up/i }));

        expect(await screen.findByText('サインアップに失敗しました')).toBeInTheDocument();
    });
});

describe('SignUpPage バリデーション', () => {
    it('短すぎるパスワードの場合エラーメッセージを表示する', async () => {
        const user = userEvent.setup();
        render(<SignUpPage />);

        // 入力操作(パスワードが短すぎる)
        await user.type(screen.getByPlaceholderText('test@example.com'), 'user@test.com');
        await user.type(screen.getByPlaceholderText('********'), '123');

        // ボタンクリック
        await user.click(screen.getByRole('button', { name: /Sign Up/i }));

        // エラーメッセージが表示されることを確認
        expect(await screen.findByText("パスワードは8文字以上必要です")).toBeInTheDocument();
    });

    it('メールアドレス必須エラーメッセージを表示', async () => {
        const user = userEvent.setup();
        render(<SignUpPage />);

        // 入力操作(メールアドレス未入力)
        await user.type(screen.getByPlaceholderText('********'), 'secret123');

        // ボタンクリック
        await user.click(screen.getByRole('button', { name: /Sign Up/i }));

        // エラーメッセージが表示されることを確認
        expect(await screen.findByText("メールアドレスは必須です")).toBeInTheDocument();
    });

    it('メールアドレス入力形式エラーメッセージを表示', async () => {
        const user = userEvent.setup();
        render(<SignUpPage />);

        // 入力操作(メールアドレス入力形式不正)
        await user.type(screen.getByPlaceholderText('test@example.com'), 'test.com');
        await user.type(screen.getByPlaceholderText('********'), 'secret123');

        // ボタンクリック
        await user.click(screen.getByRole('button', { name: /Sign Up/i }));

        // エラーメッセージが表示されることを確認
        expect(await screen.findByText("正しいメールアドレスを入力してください")).toBeInTheDocument();
    });

    it('パスワード必須エラーメッセージを表示', async () => {
        const user = userEvent.setup();
        render(<SignUpPage />);

        // 入力操作(パスワード未入力)
        await user.type(screen.getByPlaceholderText('test@example.com'), 'user@test.com');

        // ボタンクリック
        await user.click(screen.getByRole('button', { name: /Sign Up/i }));

        // エラーメッセージが表示されることを確認
        expect(await screen.findByText("パスワードは必須です")).toBeInTheDocument();
    });
});

3.3 良く使用するマッチャー

// ========================================
// 等価性のテスト
// ========================================

// toBe: プリミティブ値の厳密等価比較(===と同じ)
// 数値、文字列、真偽値、null、undefinedなどの比較に使用
expect(2 + 2).toBe(4);                    
expect('hello').toBe('hello');            
expect(true).toBe(true);                  

// toEqual: オブジェクトや配列の内容を再帰的に比較
// プロパティの値が同じであれば、異なるオブジェクトインスタンスでもテストパス
expect({name: 'test', age: 25}).toEqual({name: 'test', age: 25}); 
expect([1, 2, 3]).toEqual([1, 2, 3]);     

// toStrictEqual: toEqualよりも厳密な比較
// undefinedプロパティやsparse配列の違いも検出
expect({name: 'test'}).toStrictEqual({name: 'test'}); 

// ========================================
// 真偽値のテスト
// ========================================

// toBeTruthy: JavaScriptで真と評価される値をテスト
// true, 1, "hello", {}, []などがパス
expect(1).toBeTruthy();                   
expect('hello').toBeTruthy();             
expect({}).toBeTruthy();                  

// toBeFalsy: JavaScriptで偽と評価される値をテスト
// false, 0, "", null, undefined, NaNがパス
expect(0).toBeFalsy();                    
expect('').toBeFalsy();                   
expect(null).toBeFalsy();                 

// toBeNull: 値がnullかどうかを厳密にテスト
expect(null).toBeNull();                  
expect(undefined).not.toBeNull();         

// toBeUndefined: 値がundefinedかどうかを厳密にテスト
expect(undefined).toBeUndefined();        
expect(null).not.toBeUndefined();         

// toBeDefined: 値がundefined以外かどうかをテスト
expect('hello').toBeDefined();            
expect(0).toBeDefined();                  
expect(null).toBeDefined();               

// ========================================
// 数値のテスト
// ========================================

// toBeGreaterThan: 指定した値より大きいかテスト
expect(10).toBeGreaterThan(5);            

// toBeGreaterThanOrEqual: 指定した値以上かテスト
expect(10).toBeGreaterThanOrEqual(10);    

// toBeLessThan: 指定した値より小さいかテスト
expect(5).toBeLessThan(10);               

// toBeLessThanOrEqual: 指定した値以下かテスト
expect(5).toBeLessThanOrEqual(5);         

// toBeCloseTo: 浮動小数点数の近似値比較(第2引数は小数点以下の桁数)
// 浮動小数点の計算誤差を考慮したテストに使用
expect(0.1 + 0.2).toBeCloseTo(0.3, 5);   
expect(Math.PI).toBeCloseTo(3.14, 2);     

// toBeNaN: 値がNaN(Not a Number)かどうかをテスト
expect(Number('abc')).toBeNaN();          
expect(0 / 0).toBeNaN();                  

// ========================================
// 文字列のテスト
// ========================================

// toMatch: 文字列が正規表現にマッチするかテスト
expect('hello world').toMatch(/world/);   
expect('test@example.com').toMatch(/\w+@\w+\.\w+/); 

// toContain: 文字列が指定した部分文字列を含むかテスト
expect('hello world').toContain('world'); 
expect('JavaScript').toContain('Script'); 

// toHaveLength: 文字列の長さをテスト
expect('hello').toHaveLength(5);          

// toStartWith: 文字列が指定した文字列で始まるかテスト(カスタムマッチャー)
// toEndWith: 文字列が指定した文字列で終わるかテスト(カスタムマッチャー)

// ========================================
// 配列・反復可能オブジェクトのテスト
// ========================================

// toContain: 配列が指定した要素を含むかテスト
expect(['apple', 'banana', 'orange']).toContain('apple'); 
expect([1, 2, 3, 4, 5]).toContain(3);     

// toHaveLength: 配列やオブジェクトの長さ・要素数をテスト
expect(['a', 'b', 'c']).toHaveLength(3);  
expect('hello').toHaveLength(5);          

// toContainEqual: 配列が指定したオブジェクトと等価な要素を含むかテスト
expect([{id: 1}, {id: 2}]).toContainEqual({id: 1}); 

// arrayContaining: 配列が指定した要素を部分的に含むかテスト
expect(['a', 'b', 'c']).toEqual(expect.arrayContaining(['a', 'c'])); 

// ========================================
// オブジェクトのテスト
// ========================================

// toHaveProperty: オブジェクトが指定したプロパティを持つかテスト
expect({name: 'test', age: 25}).toHaveProperty('name'); 
expect({user: {name: 'test'}}).toHaveProperty('user.name', 'test'); 

// toMatchObject: オブジェクトが指定したプロパティとその値を持つかテスト
// 完全一致ではなく、指定したプロパティのみをチェック
expect({name: 'test', age: 25, city: 'Tokyo'}).toMatchObject({name: 'test', age: 25}); 

// objectContaining: オブジェクトが指定したプロパティを部分的に含むかテスト
expect({name: 'test', age: 25}).toEqual(expect.objectContaining({name: 'test'})); 

// ========================================
// 例外(エラー)のテスト
// ========================================

// toThrow: 関数が例外を投げるかテスト(例外メッセージの指定なし)
expect(() => {
  throw new Error('Something went wrong');
}).toThrow(); 

// toThrow: 特定のエラーメッセージを投げるかテスト
expect(() => {
  throw new Error('ファイルが見つかりません');
}).toThrow('ファイルが見つかりません'); 

// toThrow: 正規表現でエラーメッセージをテスト
expect(() => {
  throw new Error('Error: Invalid input');
}).toThrow(/Invalid/); 

// toThrow: 特定のエラークラスを投げるかテスト
expect(() => {
  throw new TypeError('型エラー');
}).toThrow(TypeError); 

// ========================================
// 非同期処理のテスト
// ========================================

// resolves: Promiseが正常に解決される値をテスト
await expect(Promise.resolve('成功')).resolves.toBe('成功'); 
await expect(fetchUserData(1)).resolves.toHaveProperty('name'); 

// rejects: Promiseが拒否される値をテスト
await expect(Promise.reject('エラー')).rejects.toMatch('エラー'); 
await expect(fetchUserData(-1)).rejects.toThrow('無効なユーザーID'); 

// ========================================
// 関数・モックのテスト
// ========================================

// toHaveBeenCalled: モック関数が呼び出されたかテスト
const mockFn = jest.fn();
mockFn();
expect(mockFn).toHaveBeenCalled(); 

// toHaveBeenCalledTimes: モック関数が指定回数呼び出されたかテスト
const mockFn2 = jest.fn();
mockFn2();
mockFn2();
expect(mockFn2).toHaveBeenCalledTimes(2); 

// toHaveBeenCalledWith: モック関数が指定した引数で呼び出されたかテスト
const mockFn3 = jest.fn();
mockFn3('test', 123);
expect(mockFn3).toHaveBeenCalledWith('test', 123); 

// toHaveBeenLastCalledWith: モック関数の最後の呼び出しの引数をテスト
const mockFn4 = jest.fn();
mockFn4('first');
mockFn4('last');
expect(mockFn4).toHaveBeenLastCalledWith('last'); 

// toHaveBeenNthCalledWith: モック関数のN回目の呼び出しの引数をテスト
const mockFn5 = jest.fn();
mockFn5('first');
mockFn5('second');
expect(mockFn5).toHaveBeenNthCalledWith(1, 'first'); 
expect(mockFn5).toHaveBeenNthCalledWith(2, 'second'); 

// toHaveReturnedWith: モック関数が指定した値を返したかテスト
const mockFn6 = jest.fn(() => 'result');
mockFn6();
expect(mockFn6).toHaveReturnedWith('result'); 

// ========================================
// インスタンス・型のテスト
// ========================================

// toBeInstanceOf: 値が指定したクラスのインスタンスかテスト
expect(new Date()).toBeInstanceOf(Date); 
expect([]).toBeInstanceOf(Array); 
expect({}).toBeInstanceOf(Object); 

// ========================================
// 否定のテスト
// ========================================

// not: 任意のマッチャーの結果を否定
expect(2 + 2).not.toBe(5);               // 4は5ではない
expect('hello').not.toContain('xyz');    // "hello"は"xyz"を含まない
expect([1, 2, 3]).not.toHaveLength(5);   // 配列の長さは5ではない

// ========================================
// 実践的な使用例
// ========================================

// 複合的なオブジェクトのテスト例
const user = {
  id: 1,
  name: '田中太郎',
  email: 'tanaka@example.com',
  profile: {
    age: 30,
    city: '東京'
  },
  hobbies: ['読書', '映画鑑賞']
};

expect(user).toHaveProperty('id', 1);                           // IDが1
expect(user).toHaveProperty('profile.age', 30);                // ネストしたプロパティ
expect(user.hobbies).toContain('読書');                        // 配列に特定要素
expect(user.hobbies).toHaveLength(2);                          // 配列の長さ
expect(user).toMatchObject({name: '田中太郎', email: expect.stringContaining('@')}); // 部分マッチ

// API レスポンスのテスト例
const apiResponse = {
  status: 200,
  data: {
    users: [
      {id: 1, name: 'User1'},
      {id: 2, name: 'User2'}
    ]
  },
  timestamp: new Date()
};

expect(apiResponse.status).toBe(200);                          // ステータスコード
expect(apiResponse.data.users).toHaveLength(2);               // ユーザー数
expect(apiResponse.data.users[0]).toHaveProperty('id');       // 各ユーザーにIDあり
expect(apiResponse.timestamp).toBeInstanceOf(Date);           // タイムスタンプが日付オブジェクト

4.Jest実行

npm run test 

 PASS  src/app/__tests__/signup.test.tsx
-----------|---------|----------|---------|---------|-------------------
File       | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
-----------|---------|----------|---------|---------|-------------------
All files  |     100 |      100 |     100 |     100 |                   
 signup    |     100 |      100 |     100 |     100 |                   
  page.tsx |     100 |      100 |     100 |     100 |                   
-----------|---------|----------|---------|---------|-------------------

Test Suites: 1 passed, 1 total
Tests:       11 passed, 11 total
Snapshots:   0 total
Time:        1.47 s
Ran all test suites.

5.まとめ

本記事を作成した中で、Jestの理解が少し深まりました。
Claude Codeの実装内容を把握出来る状態になって、
まずは上記のよく使うマッチャーの理解を深めていきたいと思います。

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?