T-CREATOR

Jest のパラメータライズドテスト(each)徹底活用術

Jest のパラメータライズドテスト(each)徹底活用術

テストコードを書いていると、似たような処理を何度も書くことになり、「もっと効率的に書けないかな?」と感じることはありませんか?

特に複数の入力値に対して同じテストロジックを適用したい場合、従来のテストコードでは冗長になりがちです。そんな課題を解決してくれるのが、Jest の**パラメータライズドテスト(each)**です。

この記事では、Jest のtest.eachdescribe.eachを使って、テストコードの重複を削減し、保守性を向上させる実践的な手法をご紹介します。初心者の方でも理解しやすいよう、具体的なコード例とともに、段階的に解説していきますね。

パラメータライズドテストとは

従来のテストコードの課題

従来のテストコードでは、同じロジックを異なる入力値で検証する際、以下のような問題が発生していました。

まず、重複コードの問題を見てみましょう。

javascript// 従来の冗長なテストコード例
describe('計算機能のテスト', () => {
  test('1 + 2 = 3', () => {
    expect(add(1, 2)).toBe(3);
  });

  test('5 + 3 = 8', () => {
    expect(add(5, 3)).toBe(8);
  });

  test('10 + 15 = 25', () => {
    expect(add(10, 15)).toBe(25);
  });
});

このように、同じテストロジックを複数回書くことで、以下の問題が生じます:

#課題具体的な問題
1コードの重複同じパターンのテストが量産される
2保守性の低下ロジック変更時に複数箇所の修正が必要
3可読性の悪化テストケースの全体像が把握しにくい
4追加コスト新しいテストケースの追加に手間がかかる

each を使った解決アプローチ

パラメータライズドテストは、これらの課題を根本的に解決します。テストデータとテストロジックを分離することで、より効率的なテストコードが書けるようになります。

javascript// パラメータライズドテストによる改善例
describe('計算機能のテスト', () => {
  test.each([
    [1, 2, 3],
    [5, 3, 8],
    [10, 15, 25],
  ])('add(%i, %i) = %i', (a, b, expected) => {
    expect(add(a, b)).toBe(expected);
  });
});

この手法により、テストケースの追加が簡単になり、保守性が大幅に向上します。

Jest の test.each と describe.each の基本

test.each の基本構文

test.eachは、個別のテストケースをパラメータ化するための機能です。配列形式とテンプレート形式の 2 つの書き方があります。

まず、配列形式の基本的な使い方を見てみましょう。

javascript// 配列形式の基本構文
test.each([
  [input1, input2, expected],
  [input3, input4, expected2],
  // ... 追加のテストケース
])('テストケース名: %s', (param1, param2, expected) => {
  // テストの実行内容
  expect(functionUnderTest(param1, param2)).toBe(expected);
});

実際のバリデーション関数のテストで見てみると、以下のようになります:

javascript// メールアドレスバリデーションのテスト例
const validateEmail = (email) => {
  const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return regex.test(email);
};

test.each([
  ['test@example.com', true],
  ['invalid-email', false],
  ['user@domain.co.jp', true],
  ['@invalid.com', false],
  ['user@', false],
])(
  'validateEmail("%s") は %s を返す',
  (email, expected) => {
    expect(validateEmail(email)).toBe(expected);
  }
);

describe.each の基本構文

describe.eachは、テストスイート全体をパラメータ化する場合に使用します。複数のテストケースを同じパラメータで実行したい場合に便利です。

javascript// describe.each の基本構文
describe.each([
  [param1, param2],
  [param3, param4],
])('テストスイート名: %s', (param1, param2) => {
  test('テストケース1', () => {
    // param1, param2を使ったテスト
  });

  test('テストケース2', () => {
    // param1, param2を使った別のテスト
  });
});

ユーザー権限のテストを例に見てみましょう:

javascript// ユーザー権限テストの例
describe.each([
  ['admin', { canEdit: true, canDelete: true }],
  ['editor', { canEdit: true, canDelete: false }],
  ['viewer', { canEdit: false, canDelete: false }],
])('"%s" 権限のテスト', (role, permissions) => {
  test('編集権限の確認', () => {
    expect(hasPermission(role, 'edit')).toBe(
      permissions.canEdit
    );
  });

  test('削除権限の確認', () => {
    expect(hasPermission(role, 'delete')).toBe(
      permissions.canDelete
    );
  });
});

配列形式とテンプレート形式の違い

Jest のパラメータライズドテストには、配列形式とテンプレート形式の 2 つの記法があります。それぞれの特徴を理解して、適切に使い分けましょう。

配列形式は、シンプルなテストケースに適しています:

javascript// 配列形式 - シンプルで直感的
test.each([
  [1, 2, 3],
  [2, 3, 5],
  [3, 4, 7],
])('add(%i, %i) = %i', (a, b, sum) => {
  expect(add(a, b)).toBe(sum);
});

一方、テンプレート形式は、より複雑なテストデータを扱う場合に威力を発揮します:

javascript// テンプレート形式 - 複雑なデータ構造に対応
test.each`
  name      | age   | expected
  ${'太郎'} | ${25} | ${'太郎さんは25歳です'}
  ${'花子'} | ${30} | ${'花子さんは30歳です'}
  ${'次郎'} | ${22} | ${'次郎さんは22歳です'}
`('$name のプロフィール表示', ({ name, age, expected }) => {
  expect(createProfile(name, age)).toBe(expected);
});

テンプレート形式の利点は、テストデータの構造が一目で分かることです。特に多くのパラメータを扱う場合、可読性が格段に向上します。

実践的な活用パターン

複数の入力値でのバリデーションテスト

実際の開発では、フォームのバリデーション処理をテストする機会が多いでしょう。パラメータライズドテストを使えば、様々なエッジケースを効率的に検証できます。

まず、パスワードバリデーションの例を見てみましょう:

javascript// パスワードバリデーション関数
const validatePassword = (password) => {
  if (password.length < 8) {
    return {
      valid: false,
      error: 'パスワードは8文字以上である必要があります',
    };
  }
  if (!/[A-Z]/.test(password)) {
    return {
      valid: false,
      error: '大文字を含む必要があります',
    };
  }
  if (!/[0-9]/.test(password)) {
    return {
      valid: false,
      error: '数字を含む必要があります',
    };
  }
  return { valid: true, error: null };
};

このバリデーション関数に対して、従来の方法では以下のような冗長なテストコードになってしまいます:

javascript// 従来の冗長なテストコード
describe('パスワードバリデーション', () => {
  test('8文字未満の場合はエラー', () => {
    const result = validatePassword('Ab1');
    expect(result.valid).toBe(false);
    expect(result.error).toBe(
      'パスワードは8文字以上である必要があります'
    );
  });

  test('大文字がない場合はエラー', () => {
    const result = validatePassword('abcdefg1');
    expect(result.valid).toBe(false);
    expect(result.error).toBe('大文字を含む必要があります');
  });

  // ... 他のテストケースも同様に冗長
});

パラメータライズドテストを使えば、格段にシンプルになります:

javascript// パラメータライズドテストによる改善
describe('パスワードバリデーション', () => {
  test.each([
    [
      'Ab1',
      false,
      'パスワードは8文字以上である必要があります',
    ],
    ['abcdefg1', false, '大文字を含む必要があります'],
    ['ABCDEFG1', false, '小文字を含む必要があります'],
    ['AbcdefgH', false, '数字を含む必要があります'],
    ['Abcdefg1', true, null],
    ['StrongPass123', true, null],
  ])(
    'validatePassword("%s")',
    (password, expectedValid, expectedError) => {
      const result = validatePassword(password);
      expect(result.valid).toBe(expectedValid);
      expect(result.error).toBe(expectedError);
    }
  );
});

異なる条件での計算結果テスト

ビジネスロジックのテストでは、異なる条件での計算結果を検証する必要があります。消費税計算を例に見てみましょう:

javascript// 消費税計算関数
const calculateTax = (amount, rate, isReduced = false) => {
  const taxRate = isReduced ? 0.08 : rate;
  return Math.floor(amount * taxRate);
};

// 複数の税率と軽減税率の組み合わせテスト
test.each([
  [1000, 0.1, false, 100], // 標準税率
  [1000, 0.1, true, 80], // 軽減税率
  [1500, 0.1, false, 150], // 標準税率(異なる金額)
  [1500, 0.1, true, 120], // 軽減税率(異なる金額)
  [333, 0.1, false, 33], // 端数処理のテスト
  [333, 0.1, true, 26], // 端数処理のテスト(軽減税率)
])(
  'calculateTax(%i, %f, %s) = %i',
  (amount, rate, isReduced, expected) => {
    expect(calculateTax(amount, rate, isReduced)).toBe(
      expected
    );
  }
);

このように、複数の条件を組み合わせたテストケースを効率的に実行できます。

API レスポンスの検証テスト

API のテストでは、異なるステータスコードやレスポンス形式を検証する必要があります。以下は、ユーザー情報取得 API のテスト例です:

javascript// APIレスポンスのモック
const mockApiResponse = {
  200: { id: 1, name: '太郎', email: 'taro@example.com' },
  404: { error: 'ユーザーが見つかりません' },
  500: { error: 'サーバーエラーが発生しました' },
};

// fetchUser関数のモック実装
const fetchUser = async (userId) => {
  // 実際のAPIコールをシミュレート
  if (userId === 1)
    return { status: 200, data: mockApiResponse[200] };
  if (userId === 999)
    return { status: 404, data: mockApiResponse[404] };
  return { status: 500, data: mockApiResponse[500] };
};

この API に対するテストを、パラメータライズドテストで効率的に書けます:

javascriptdescribe('fetchUser API', () => {
  test.each([
    [1, 200, mockApiResponse[200]],
    [999, 404, mockApiResponse[404]],
    [0, 500, mockApiResponse[500]],
  ])(
    'ユーザーID %i のリクエスト',
    async (userId, expectedStatus, expectedData) => {
      const response = await fetchUser(userId);
      expect(response.status).toBe(expectedStatus);
      expect(response.data).toEqual(expectedData);
    }
  );
});

エラーケースも含めて、様々な API レスポンスを一括でテストできるため、テストの網羅性が向上します。

高度な活用テクニック

複雑なオブジェクトのテストデータ管理

実際のアプリケーションでは、複雑なオブジェクト構造を扱うことが多いです。そのようなケースでも、パラメータライズドテストは威力を発揮します。

ユーザープロファイルの検証関数を例に見てみましょう:

javascript// ユーザープロファイル検証関数
const validateUserProfile = (profile) => {
  const errors = [];

  if (!profile.name || profile.name.length < 2) {
    errors.push('名前は2文字以上で入力してください');
  }

  if (
    !profile.email ||
    !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(profile.email)
  ) {
    errors.push('有効なメールアドレスを入力してください');
  }

  if (profile.age < 13 || profile.age > 120) {
    errors.push(
      '年齢は13歳から120歳の間で入力してください'
    );
  }

  return {
    valid: errors.length === 0,
    errors: errors,
  };
};

このような複雑な関数に対して、テンプレート形式でテストデータを管理できます:

javascript// 複雑なオブジェクトのテストデータ管理
describe('ユーザープロファイル検証', () => {
  test.each`
    name      | email                   | age    | expectedValid | expectedErrors
    ${'太郎'} | ${'taro@example.com'}   | ${25}  | ${true}       | ${[]}
    ${'花'}   | ${'hanako@test.com'}    | ${30}  | ${false}      | ${['名前は2文字以上で入力してください']}
    ${'次郎'} | ${'invalid-email'}      | ${22}  | ${false}      | ${['有効なメールアドレスを入力してください']}
    ${'佐藤'} | ${'sato@example.com'}   | ${12}  | ${false}      | ${['年齢は13歳から120歳の間で入力してください']}
    ${'田中'} | ${'tanaka@example.com'} | ${121} | ${false}      | ${['年齢は13歳から120歳の間で入力してください']}
  `(
    '$name のプロファイル検証',
    ({
      name,
      email,
      age,
      expectedValid,
      expectedErrors,
    }) => {
      const profile = { name, email, age };
      const result = validateUserProfile(profile);

      expect(result.valid).toBe(expectedValid);
      expect(result.errors).toEqual(expectedErrors);
    }
  );
});

このように、複雑なテストデータも整理された形で管理できます。

非同期処理との組み合わせ

非同期処理を含むテストでも、パラメータライズドテストは有効です。データベースアクセスや API 呼び出しなど、様々な非同期処理のテストケースを効率的に書けます。

javascript// 非同期でのユーザー作成機能
const createUser = async (userData) => {
  // データベースへの保存処理をシミュレート
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (userData.email === 'duplicate@example.com') {
        reject(new Error('EMAIL_DUPLICATE'));
      } else if (userData.name === '') {
        reject(new Error('NAME_REQUIRED'));
      } else {
        resolve({ id: Math.random(), ...userData });
      }
    }, 100);
  });
};

// 非同期処理のパラメータライズドテスト
describe('ユーザー作成機能', () => {
  test.each([
    [
      { name: '太郎', email: 'taro@example.com' },
      'success',
      null,
    ],
    [
      { name: '花子', email: 'duplicate@example.com' },
      'error',
      'EMAIL_DUPLICATE',
    ],
    [
      { name: '', email: 'test@example.com' },
      'error',
      'NAME_REQUIRED',
    ],
  ])(
    'createUser(%j)',
    async (userData, expectedResult, expectedError) => {
      if (expectedResult === 'success') {
        const result = await createUser(userData);
        expect(result).toHaveProperty('id');
        expect(result.name).toBe(userData.name);
        expect(result.email).toBe(userData.email);
      } else {
        await expect(createUser(userData)).rejects.toThrow(
          expectedError
        );
      }
    }
  );
});

モック関数との連携

モック関数とパラメータライズドテストを組み合わせることで、より柔軟なテストが可能になります。

javascript// ログ出力機能のテスト
const logger = {
  info: jest.fn(),
  error: jest.fn(),
  warn: jest.fn(),
};

const logMessage = (level, message) => {
  switch (level) {
    case 'info':
      logger.info(message);
      break;
    case 'error':
      logger.error(message);
      break;
    case 'warn':
      logger.warn(message);
      break;
    default:
      throw new Error(`Unknown log level: ${level}`);
  }
};

// モック関数を使ったパラメータライズドテスト
describe('ログ機能', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  test.each([
    ['info', 'Information message', 'info'],
    ['error', 'Error message', 'error'],
    ['warn', 'Warning message', 'warn'],
  ])(
    'logMessage("%s", "%s")',
    (level, message, expectedMethod) => {
      logMessage(level, message);
      expect(logger[expectedMethod]).toHaveBeenCalledWith(
        message
      );
      expect(logger[expectedMethod]).toHaveBeenCalledTimes(
        1
      );
    }
  );
});

パフォーマンスと保守性の向上

テストコードの可読性向上

パラメータライズドテストを使用する際の可読性向上のポイントを以下の表にまとめました:

#ポイント説明
1分かりやすいテスト名テストケースの内容が分かるように命名'add(%i, %i) = %i'
2適切なパラメータ名意味のある変数名を使用(input, expected)
3コメントの活用複雑なテストケースには説明を追加​/​​/​ 境界値テスト
4データの整理関連するテストケースをグループ化テンプレート形式の活用

実際のコード例で見てみましょう:

javascript// 可読性の高いパラメータライズドテスト
describe('日付フォーマット関数', () => {
  test.each`
    input                     | format          | expected
    ${new Date('2023-12-25')} | ${'YYYY-MM-DD'} | ${'2023-12-25'}
    ${new Date('2023-12-25')} | ${'MM/DD/YYYY'} | ${'12/25/2023'}
    ${new Date('2023-12-25')} | ${'DD-MM-YYYY'} | ${'25-12-2023'}
  `(
    'formatDate($input, "$format") は "$expected" を返す',
    ({ input, format, expected }) => {
      expect(formatDate(input, format)).toBe(expected);
    }
  );
});

データ駆動テストの実装

大量のテストデータを外部ファイルから読み込んで、データ駆動テストを実装することも可能です:

javascript// テストデータを外部ファイルから読み込み
const testData = require('./test-data.json');

describe('商品価格計算', () => {
  test.each(testData.priceCalculation)(
    'calculatePrice($basePrice, $discountRate, $taxRate) = $expected',
    ({ basePrice, discountRate, taxRate, expected }) => {
      const result = calculatePrice(
        basePrice,
        discountRate,
        taxRate
      );
      expect(result).toBe(expected);
    }
  );
});

対応する JSON ファイル(test-data.json):

json{
  "priceCalculation": [
    {
      "basePrice": 1000,
      "discountRate": 0.1,
      "taxRate": 0.1,
      "expected": 990
    },
    {
      "basePrice": 2000,
      "discountRate": 0.2,
      "taxRate": 0.1,
      "expected": 1760
    }
  ]
}

テストケースの動的生成

場合によっては、テストケースを動的に生成する必要があります。以下は、ランダムな値を使ったテストケースの例です:

javascript// 動的テストケース生成の例
const generateRandomTestCases = (count) => {
  return Array.from({ length: count }, (_, i) => {
    const a = Math.floor(Math.random() * 100);
    const b = Math.floor(Math.random() * 100);
    return [a, b, a + b];
  });
};

describe('加算機能のランダムテスト', () => {
  test.each(generateRandomTestCases(10))(
    'add(%i, %i) = %i',
    (a, b, expected) => {
      expect(add(a, b)).toBe(expected);
    }
  );
});

ただし、動的生成を使用する際は、以下の点に注意が必要です:

  • テストの再現性を保つため、シードを固定する
  • 生成されるテストケースが適切な範囲をカバーしているか確認する
  • デバッグが困難になる可能性があることを考慮する

まとめ

Jest のパラメータライズドテスト(each)は、テストコードの品質と効率を大幅に向上させる強力な機能です。

この記事で学んだ主なポイントを振り返ってみましょう:

基本的な活用方法

  • test.eachdescribe.eachの基本構文をマスターする
  • 配列形式とテンプレート形式を適切に使い分ける
  • テストデータとロジックを分離して可読性を向上させる

実践的な活用パターン

  • バリデーション関数の網羅的なテスト
  • ビジネスロジックの条件分岐テスト
  • API レスポンスの検証テスト

高度なテクニック

  • 複雑なオブジェクト構造のテストデータ管理
  • 非同期処理との組み合わせ
  • モック関数との連携

保守性の向上

  • 可読性の高いテストコードの書き方
  • データ駆動テストの実装
  • テストケースの動的生成

パラメータライズドテストを適切に活用することで、テストコードの重複を削減し、保守性を向上させることができます。また、テストケースの追加が簡単になるため、より網羅的なテストを効率的に実装できるようになります。

最初は慣れないかもしれませんが、一度コツを掴めば、テストコードの品質が格段に向上することを実感できるでしょう。ぜひ、実際のプロジェクトでパラメータライズドテストを活用してみてください。

あなたのテストコードがより効率的で保守しやすくなることを願っています。

関連リンク