😊

【TypeScript/ハンズオン】テスト駆動開発(TDD)入門 第2回:Jestではじめるテスト開発

に公開

はじめに

前回は、TDDの概念と基本的な流れについて学びました。
https://zenn.dev/nezumizuki/articles/fa821accc95050
今回は、実際にTypeScriptとJestを使ってテストを書いていく方法を学んでいきます。

Jestとは特にJavaScript/TypeScript向けに設計された人気のテストフレームワークです。
Facebookがオープンソースとして開発しています。
TypeScript × Jest の組み合わせは現代のWeb開発では非常によく使われています。

前提条件

このチュートリアルを進めるには、以下が必要です

  1. Node.jsがインストールされていること

    • バージョン確認: ターミナルで node -v を実行
    • インストールされていない場合は、Node.js公式サイトからダウンロード
  2. npmがインストールされていること(Node.jsと一緒にインストールされます)

    • バージョン確認: npm -v を実行
  3. 好みのコードエディタ(Visual Studio Code推奨)

1. 開発環境のセットアップ

まずは開発環境を整えていきましょう。以下の手順を順番に実行していきます。

1.1 プロジェクトの初期化

ターミナルを開いて、以下のコマンドを順番に実行します

# プロジェクトディレクトリの作成と移動
mkdir tdd-tutorial
cd tdd-tutorial

# package.jsonの作成(すべてデフォルト値でOK)
npm init -y

# 必要なパッケージのインストール(少し時間がかかります)
npm install --save-dev typescript ts-jest @types/jest jest

それぞれのパッケージの役割を説明します

  • typescript: TypeScriptコンパイラ本体
  • ts-jest: TypeScriptでJestを使うためのブリッジ
  • @types/jest: JestのTypeScript型定義
  • jest: テストフレームワーク本体

インストールが完了したら、package.jsonに必要なパッケージが追加されていることを確認しましょう

{
  "devDependencies": {
    "@types/jest": "^29.5.12",
    "jest": "^29.7.0",
    "ts-jest": "^29.1.2",
    "typescript": "^5.3.3"
  }
}

1.2 TypeScript設定ファイルの作成

次に、TypeScriptの設定ファイルを作成します

# tsconfig.jsonの作成
npx tsc --init

tsconfig.jsonが作成されたら、以下の設定を確認

{
  "compilerOptions": {
    "target": "es2020",       // コンパイル後のJavaScriptバージョン
    "module": "commonjs",     // モジュールシステムの種類
    "strict": true,          // 厳格な型チェックを有効化
    "esModuleInterop": true, // import文の互換性向上
    "skipLibCheck": true,    // 型定義ファイルのチェックをスキップ
    "forceConsistentCasingInFileNames": true  // ファイル名の大文字小文字を厳格にチェック
  }
}

設定の意味

  • target: どのバージョンのJavaScriptにコンパイルするか
  • module: どのような形式でモジュールを扱うか
  • strict: 型チェックをより厳密に行うかどうか

1.3 Jest設定ファイルの作成

最後に、Jestの設定ファイルを作成します

# jest.config.jsの作成
npx ts-jest config:init

この時点でのプロジェクト構造

tdd-tutorial/
├── node_modules/
├── jest.config.js
├── package.json
├── package-lock.json
└── tsconfig.json

2. はじめてのテストケース

環境が整ったので、実際にテストを書いていきましょう。
最初は、文字列を逆順に入れ替えるreverseString関数を例に実装します。

2.1 ディレクトリ構造の作成

まず、ソースコードとテストを配置するディレクトリを作成します

# srcディレクトリを作成
mkdir src

2.2 最初のテストファイルを作成

VSCodeなどのエディタでプロジェクトを開き、以下のファイルを作成します

// src/reverseString.test.ts
import { reverseString } from './reverseString';

describe('reverseString', () => {
    test('reverses a simple string', () => {
        expect(reverseString('hello')).toBe('olleh');
    });
});

解説

  • import { reverseString }: 実装する関数をインポート
  • describe: テストのグループを作る関数。関連するテストをまとめる
  • test: 個別のテストケースを定義する関数
  • expect(reverseString('hello')): テスト対象の関数を実行
  • toBe('olleh'): 期待される結果を指定

2.3 実装ファイルの作成

次に、実装ファイルを作成します

// src/reverseString.ts
export function reverseString(str: string): string {
    return str.split('').reverse().join('');
}

解説

  1. split(''): 文字列を1文字ずつ配列に分割
    • 例: 'hello' → ['h', 'e', 'l', 'l', 'o']
  2. reverse(): 配列の要素を逆順に並び替え
    • 例: ['h', 'e', 'l', 'l', 'o'] → ['o', 'l', 'l', 'e', 'h']
  3. join(''): 配列を文字列に結合
    • 例: ['o', 'l', 'l', 'e', 'h'] → 'olleh'

2.4 テストの実行

テストを実行するために、package.jsonのscriptsセクションを編集してください

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

テストを実行するには

# 全てのテストを1回実行
npm test

# テストを監視モードで実行
npm run test:watch  # 実行後ターミナルをそのままにしておき、ファイルの変更を検知して自動実行

注意: npmコマンドの実行について

  • npm testは特別な省略形で、npm run testと同じ意味です
  • その他のカスタムスクリプト(test:watchなど)を実行する場合は、必ずnpm runをつける必要があります
  • 間違ってnpm test:watchと実行すると「Unknown command」エラーが発生します

もし「Unknown command: "test:watch"」というエラーが出た場合は、npm run test:watchを使用してください。

3. Jestの基本的な機能

ここからは、Jestの主要な機能を実践的に説明します。

3.1 テストスイートの構造化

テストを整理して管理しやすくするために、Jestではテストをグループ化できます。
以下のファイルを作成してみましょう

// src/reverseString.test.ts
describe('reverseString', () => {
    // beforeEach: 各テストケースの前に実行
    beforeEach(() => {
        console.log('テストケース開始');
    });

    // afterEach: 各テストケースの後に実行
    afterEach(() => {
        console.log('テストケース終了');
    });

    // ネストされたdescribeでテストをグループ化
    describe('basic functionality', () => {
        test('reverses a simple string', () => {
            expect(reverseString('hello')).toBe('olleh');
        });

        test('reverses a string with spaces', () => {
            expect(reverseString('hello world')).toBe('dlrow olleh');
        });
    });
});

《解説》

  • beforeEach/afterEach: 各テストケースの前後で実行される処理を定義
    • データベースのセットアップ/クリーンアップなどに使用
    • テスト結果には表示されない準備/後片付けの処理を書く場所
  • ネストされたdescribe: 関連するテストをさらにグループ化
    • 機能ごと、条件ごとにテストを整理できる
    • コードの見通しが良くなる

実行結果は以下のようになります

 PASS  src/reverseString.test.ts
  reverseString
    basic functionality
      ✓ reverses a simple string
      ✓ reverses a string with spaces

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total

3.2 よく使うマッチャー(検証メソッド)

Jestには様々な検証メソッド(マッチャー)が用意されています。実際に使ってみましょう

// src/reverseString.test.ts
import { reverseString } from './reverseString';

describe('reverseString', () => {
    // 既存のテストケース

    describe('Jest matchers examples', () => {
        // 厳密な等価性
        test('exact matching', () => {
            expect(reverseString('hello')).toBe('olleh');  // 文字列、数値、真偽値の比較
    });

        // オブジェクトの比較
        test('object matching', () => {
            const result = { text: reverseString('hello') };
            expect(result).toEqual({ text: 'olleh' });  // オブジェクトの内容比較
        });

        // 部分一致
        test('string containing', () => {
            const result = reverseString('hello');
            expect(result).toContain('ll');  // 部分文字列を含むか
        });

        // 真偽値
        test('truthiness', () => {
            const result = reverseString('hello');
            expect(result).toBeTruthy();  // null, undefined, false以外
            expect(result.length).toBeGreaterThan(0);  // 数値の比較
        });
    });
});

《解説》

  • toBe(): プリミティブ値(文字列、数値、真偽値)の比較
  • toEqual(): オブジェクトや配列の内容比較
  • toContain(): 配列や文字列に特定の要素/部分文字列が含まれているか
  • toBeTruthy()/toBeFalsy(): 真偽値の評価
  • toBeGreaterThan()/toBeLessThan(): 数値の比較

3.3 非同期処理のテスト

実際のアプリケーションでは非同期処理が多用されます。以下は非同期処理のテスト方法です

// src/asyncReverse.test.ts
import { asyncReverseString } from './asyncReverse';

describe('asyncReverseString', () => {
    // 非同期テストの書き方1: async/await
    test('reverses string asynchronously', async () => {
        const result = await asyncReverseString('hello');
        expect(result).toBe('olleh');
    });

    // 非同期テストの書き方2: Promiseを返す
    test('reverses string asynchronously with promise', () => {
        return asyncReverseString('hello').then(result => {
            expect(result).toBe('olleh');
        });
    });
});

// src/asyncReverse.ts
export async function asyncReverseString(str: string): Promise<string> {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve(str.split('').reverse().join(''));
        }, 100);
    });
}

《注意点》

  • 非同期テストでは必ずasync/awaitを使うか、Promiseを返す
  • そうしないとテストが完了する前にJestが終了してしまう
  • タイムアウトを設定する場合はtest('description', async () => {}, timeout)の形で指定

3.4 エラーのテスト

エラーが適切に発生することも重要なテストケースです

// src/reverseString.test.ts
import { reverseString } from './reverseString';

describe('reverseString', () => {
    // 既存のテストケース
    describe('basic functionality', () => {
        // ... 既存のテスト
    });

    // エラーテストを追加
    describe('error handling', () => {
        test('throws error for null input', () => {
            expect(() => {
                reverseString(null as any);
            }).toThrow('Input must be a string');
        });

        test('throws error for undefined input', () => {
            expect(() => {
                reverseString(undefined as any);
            }).toThrow('Input must be a string');
        });
    });
});

// src/reverseString.ts
export function reverseString(str: string): string {
    // 入力値の検証を追加
    if (typeof str !== 'string') {
        throw new Error('Input must be a string');
    }
    return str.split('').reverse().join('');
}

《ポイント》

  • テストは既存のreverseString.test.tsファイルに追加
  • エラーテスト用に新しいdescribeブロックを作成
  • 実装ファイル(reverseString.ts)に入力値の検証を追加

《注意点》

  • エラーをテストする場合は、関数をexpect()で囲む
  • toThrow()にエラーメッセージを渡すと、メッセージの一致もテスト
  • 正規表現を使用してエラーメッセージをテストすることも可能

4. デバッグ方法

テストのデバッグ方法をいくつか紹介します

4.1 コンソール出力を使用

// src/reverseString.test.ts
import { reverseString } from './reverseString';

describe('reverseString', () => {
    // 既存のテストケース
    describe('basic functionality', () => {
        // ... 既存のテスト
    });

    describe('error handling', () => {
        // ... エラーテスト
    });

    // デバッグ用のテストを追加
    describe('debugging example', () => {
        test('debug with console', () => {
            const input = 'hello';
            console.log('Input:', input);  // テスト中の値を確認
            
            const result = reverseString(input);
            console.log('Result:', result);
            
            expect(result).toBe('olleh');
        });
    });
});
# 実行方法
npm test -- --verbose

4.2 特定のテストだけを実行

# 特定のファイルのみ実行
npm test -- reverseString.test.ts

# 特定のテスト名で実行(部分一致)
npm test -- -t "reverseString"

4.3 VSCodeでのデバッグ

  1. VSCodeの実行とデバッグパネルを開く
  2. launch.jsonを作成
  3. 以下の設定を追加
{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "node",
            "request": "launch",
            "name": "Debug Jest Tests",
            "program": "${workspaceFolder}/node_modules/jest/bin/jest.js",
            "args": ["--runInBand"],
            "console": "integratedTerminal",
            "internalConsoleOptions": "neverOpen"
        }
    ]
}

VScodeにて、任意の行にブレークポイントを設定する。

# 特定のテストをデバッグモードで実行
npm test -- --debug

これでブレークポイントを設定してステップ実行が可能になります。

よくあるトラブルと解決方法

Q: テストが非同期処理で失敗する
A: async/awaitを使用しているか確認。タイムアウトも適切に設定しましょう。

Q: カバレッジが100%になりません
A: 必ずしも100%を目指す必要はありません。重要なビジネスロジックのカバレッジを優先的に上げていきましょう。

Q: カバレッジレポートが欲しい
A: npm test -- --coverageを実行すると詳細なレポートが生成されます。

まとめ

今回は、TypeScriptとJestを使ったテスト開発の基本を学びました。
最初は「テストってめんどくさそう...」と感じたかもしれませんが、いかがでしたか?

👍 身につけた技術をおさらい

  • プロジェクトの土台の作り方
  • テストの書き方の基本
  • 様々な検証方法(マッチャー)の使い方
  • テストを整理して管理する方法
  • エッジケースのテスト方法
  • パフォーマンステストのコツ
  • テストカバレッジの確認方法

特に、非同期処理のテストは実践で本当によく使います。

次のステップは?

ここまでの内容を自分のプロジェクトで試してみましょう。
例えば

  • 普段書いているユーティリティ関数にテストを追加する
  • 既存のプロジェクトの一部分だけテストを書いてみる
  • 新しい機能を追加する時は、まずテストから書いてみる

完璧なテストを書こうとする必要はありません。小さな一歩から始めて、徐々にテストの書き方に慣れていけば大丈夫です。

次回は、実際のプロジェクトでよく使う「FizzBuzz」を題材に、より実践的なテスト開発に挑戦します。ぜひ続けて取り組んでみてください!

困ったときは?

テストを書いていて「あれ」と思ったら

  • この記事を読み返してみる
  • Jestの公式ドキュメントを確認する
  • コミュニティ(TypeScript Discordなど)で質問する

テスト駆動開発は最初は少し大変かもしれませんが、書けるようになると開発の質が格段に上がります。一緒に頑張っていきましょう!💪

ぜひハンズオンで実践してみてください!

次回予告

次回は、実際のプログラムをTDDで開発する方法を学びます。
題材として「FizzBuzz」を使用し、以下の内容を扱います

  1. 問題の分解方法
  2. テストケースの段階的な追加
  3. 実装の改善プロセス
  4. リファクタリングの判断基準

Discussion