T-CREATOR

【入門】Jest 初心者が最初に知っておくべきテスト設計の基本原則

【入門】Jest 初心者が最初に知っておくべきテスト設計の基本原則

JavaScript 開発において、品質の高いアプリケーションを作成するためには、テストが欠かせません。特に Jest は、その使いやすさと豊富な機能で多くの開発者に愛用されているテストフレームワークです。

しかし、いざテストを書こうとすると「何から始めればいいかわからない」「どのようにテストケースを設計すればいいかわからない」といった悩みを抱える方も多いのではないでしょうか。

今回は、Jest 初心者の方が最初に知っておくべきテスト設計の基本原則について、段階的に解説していきます。この記事を読むことで、効果的なテストの書き方と、保守しやすいテストコードの作成方法を身につけることができるでしょう。

背景

Jest とは何か

Jest は、Facebook(現 Meta)によって開発された JavaScript テストフレームワークです。シンプルな設定で動作し、多機能でありながら学習コストが低いことが特徴です。

javascript// 基本的なJestテストの例
function add(a, b) {
  return a + b;
}

test('1 + 2 は 3 になる', () => {
  expect(add(1, 2)).toBe(3);
});

Jest の主な特徴を表にまとめました。

#特徴詳細
1ゼロ設定複雑な設定なしですぐに使い始められます
2スナップショットテストUI コンポーネントの変更を検知できます
3並列実行テストを並列で実行し、実行時間を短縮できます
4モック機能外部依存をモック化してテストを独立させられます
5カバレッジレポートどの部分がテストされているかを可視化できます

なぜテスト設計が重要なのか

テスト設計が重要な理由は、単にバグを見つけるためだけではありません。以下の図でテスト設計の価値を示します。

mermaidflowchart TD
    design[テスト設計] --> quality[品質向上]
    design --> maintenance[保守性向上]
    design --> confidence[開発の自信]

    quality --> bug_reduction[バグの早期発見]
    quality --> regression[リグレッション防止]

    maintenance --> readable[読みやすいコード]
    maintenance --> modular[モジュール化促進]

    confidence --> refactoring[安全なリファクタリング]
    confidence --> feature_add[新機能追加の安心感]

図で理解できる要点

  • テスト設計は品質、保守性、開発の自信という 3 つの価値を生み出します
  • これらの価値が相互に作用し、開発全体の効率性を高めます
  • 結果として、安定したソフトウェア開発が可能になります

良いテスト設計により、以下のメリットが得られます。

  1. バグの早期発見:開発段階でバグを見つけ、修正コストを削減
  2. リファクタリングの安全性:既存機能を壊さずにコード改善が可能
  3. ドキュメント効果:テストコードが仕様書の役割を果たす
  4. 開発速度の向上:手動テストの時間を削減し、効率的な開発が可能

JavaScript 開発におけるテストの位置づけ

JavaScript 開発におけるテストは、フロントエンドからバックエンドまで幅広い領域で活用されています。

mermaidgraph LR
    subgraph frontend[フロントエンド]
        component[コンポーネントテスト]
        integration[統合テスト]
        e2e[E2Eテスト]
    end

    subgraph backend[バックエンド]
        unit[ユニットテスト]
        api[APIテスト]
        db[データベーステスト]
    end

    jest[Jest] --> component
    jest --> unit
    jest --> api

    testing_lib[Testing Library] --> component
    testing_lib --> integration

    cypress[Cypress] --> e2e
    supertest[Supertest] --> api

図で理解できる要点

  • Jest は主にユニットテストとコンポーネントテストで活用されます
  • フロントエンドでは Testing Library と組み合わせることが多いです
  • 各テストレベルで適切なツールを選択することが重要です

特に Jest は以下の分野で威力を発揮します。

  • ユニットテスト:関数やクラスの単体テスト
  • コンポーネントテスト:React コンポーネントの動作確認
  • 統合テスト:複数のモジュール間の連携テスト

課題

テストを書きたいが何から始めればよいかわからない

Jest 初心者の方が最初に直面する課題の一つが「どこから手をつければよいかわからない」ということです。

mermaidstateDiagram-v2
    [*] --> confused: テストを書きたい
    confused --> overwhelmed: 情報が多すぎる
    overwhelmed --> paralyzed: 何もできない
    paralyzed --> frustrated: 時間だけが過ぎる
    frustrated --> give_up: テストを諦める

    confused --> structured: 段階的アプローチ
    structured --> practice: 実践
    practice --> confident: 自信がつく
    confident --> [*]

この問題は、以下の要因から生じることが多いです。

#要因詳細対策
1情報過多高度なテクニックの情報が多い基本から段階的に学習
2完璧主義最初から完璧なテストを書こうとする小さく始めて徐々に改善
3実践不足理論だけで実際に書く経験が少ない簡単な例から実際に手を動かす
4目標不明確何を目指すべきかがわからない明確な学習目標を設定

テストケースの作り方がわからない

テストケースの作成において、初心者の方が迷いやすいポイントです。

javascript// 悪い例:何をテストしているかわからない
test('テスト', () => {
  const result = doSomething(input);
  expect(result).toBeTruthy();
});
javascript// 良い例:明確な意図があるテスト
test('正の数を入力した場合、その数の平方根を返す', () => {
  const result = calculateSquareRoot(9);
  expect(result).toBe(3);
});

テストケース作成でよくある問題点:

  1. テストの意図が不明確:何をテストしているかわからない
  2. テストケースが不十分:正常系のみで異常系を考慮していない
  3. テストの粒度が適切でない:一つのテストで複数のことをテストしている

どこまでテストを書けばよいかわからない

テストカバレッジの目標設定や、どの部分にテストを書くべきかの判断に迷う方が多いです。

mermaidflowchart TD
    start[テスト対象の選定] --> critical[重要度の評価]
    critical --> high[高重要度]
    critical --> medium[中重要度]
    critical --> low[低重要度]

    high --> must_test[必須テスト]
    medium --> should_test[推奨テスト]
    low --> optional_test[任意テスト]

    must_test --> business_logic[ビジネスロジック]
    must_test --> public_api[公開API]
    should_test --> util_func[ユーティリティ関数]
    optional_test --> simple_getter[単純なgetter]

図で理解できる要点

  • テスト対象を重要度で分類し、優先順位をつけます
  • ビジネスロジックや公開 API は必須でテストを書きます
  • リソースに応じて、重要度の高いものから順にテストを充実させます

適切なテスト範囲を決める指針:

  • 必須:ビジネスロジック、公開 API、複雑なアルゴリズム
  • 推奨:ユーティリティ関数、データ変換処理
  • 任意:単純な getter/setter、定数の取得

解決策

テスト設計の 3 つの基本原則

効果的なテストを書くために、以下の 3 つの基本原則を押さえておくことが重要です。

AAA(Arrange, Act, Assert)パターン

AAA パターンは、テストを 3 つの段階に分けて構造化する手法です。

javascripttest('ユーザー作成時に正しいデータが設定される', () => {
  // Arrange: テストに必要なデータや環境を準備
  const userData = {
    name: '田中太郎',
    email: 'tanaka@example.com',
    age: 30,
  };

  // Act: テスト対象の処理を実行
  const user = new User(userData);

  // Assert: 期待される結果かどうかを検証
  expect(user.name).toBe('田中太郎');
  expect(user.email).toBe('tanaka@example.com');
  expect(user.age).toBe(30);
});

AAA パターンの各段階の役割:

#段階英語役割実装のポイント
1準備Arrangeテストデータの準備必要最小限のデータを用意
2実行Actテスト対象の処理実行一つの明確なアクション
3検証Assert結果の確認期待値との比較

1 つのテストは 1 つのことをテストする

単一責任の原則をテストにも適用し、一つのテストでは一つの機能や動作のみをテストします。

javascript// 悪い例:複数のことをテストしている
test('ユーザーサービスのテスト', () => {
  const user = createUser('田中太郎');
  expect(user.name).toBe('田中太郎');

  const updated = updateUser(user, { age: 25 });
  expect(updated.age).toBe(25);

  const deleted = deleteUser(user.id);
  expect(deleted).toBe(true);
});
javascript// 良い例:それぞれ独立したテスト
test('ユーザー作成時に名前が正しく設定される', () => {
  const user = createUser('田中太郎');
  expect(user.name).toBe('田中太郎');
});

test('ユーザー更新時に年齢が正しく変更される', () => {
  const user = createUser('田中太郎');
  const updated = updateUser(user, { age: 25 });
  expect(updated.age).toBe(25);
});

test('ユーザー削除時にtrueが返される', () => {
  const user = createUser('田中太郎');
  const deleted = deleteUser(user.id);
  expect(deleted).toBe(true);
});

単一責任テストのメリット:

  • 問題の特定が容易:テストが失敗した時、原因が明確
  • 保守性の向上:変更時の影響範囲が限定的
  • 可読性の向上:テストの意図が明確

テストは独立している必要がある

各テストは他のテストの実行結果に依存せず、独立して実行できる必要があります。

javascript// 悪い例:テスト間で状態を共有
let globalUser;

test('ユーザーを作成する', () => {
  globalUser = createUser('田中太郎');
  expect(globalUser.name).toBe('田中太郎');
});

test('ユーザーを更新する', () => {
  // 前のテストに依存している
  const updated = updateUser(globalUser, { age: 25 });
  expect(updated.age).toBe(25);
});
javascript// 良い例:各テストが独立している
test('ユーザーを作成する', () => {
  const user = createUser('田中太郎');
  expect(user.name).toBe('田中太郎');
});

test('ユーザーを更新する', () => {
  // テスト内で必要なデータを準備
  const user = createUser('田中太郎');
  const updated = updateUser(user, { age: 25 });
  expect(updated.age).toBe(25);
});

テスト独立性を保つ方法:

javascript// beforeEachでテストごとに初期化
describe('ユーザーサービステスト', () => {
  let userService;

  beforeEach(() => {
    userService = new UserService();
  });

  test('ユーザー作成テスト', () => {
    const user = userService.create('田中太郎');
    expect(user.name).toBe('田中太郎');
  });

  test('ユーザー更新テスト', () => {
    const user = userService.create('田中太郎');
    const updated = userService.update(user.id, {
      age: 25,
    });
    expect(updated.age).toBe(25);
  });
});

具体例

基本的な Jest テストの書き方

describe と it の使い方

Jest では describeit(または test)を使ってテストを構造化します。

javascript// 基本的な構造
describe('電卓クラス', () => {
  it('2つの数値を足し算できる', () => {
    const calculator = new Calculator();
    const result = calculator.add(2, 3);
    expect(result).toBe(5);
  });

  it('2つの数値を引き算できる', () => {
    const calculator = new Calculator();
    const result = calculator.subtract(5, 3);
    expect(result).toBe(2);
  });
});

ネストした構造でより詳細にテストを整理できます:

javascriptdescribe('ユーザー管理システム', () => {
  describe('ユーザー作成', () => {
    it('有効なデータでユーザーを作成できる', () => {
      const userData = {
        name: '田中太郎',
        email: 'tanaka@example.com',
      };
      const user = createUser(userData);
      expect(user.name).toBe('田中太郎');
    });

    it('無効なメールアドレスでエラーが発生する', () => {
      const userData = {
        name: '田中太郎',
        email: 'invalid-email',
      };
      expect(() => createUser(userData)).toThrow(
        '無効なメールアドレス'
      );
    });
  });

  describe('ユーザー更新', () => {
    it('既存ユーザーの情報を更新できる', () => {
      const user = createUser({
        name: '田中太郎',
        email: 'tanaka@example.com',
      });
      const updated = updateUser(user.id, {
        name: '田中花子',
      });
      expect(updated.name).toBe('田中花子');
    });
  });
});

expect の基本的な使い方

Jest は豊富なマッチャーを提供しており、様々な検証が可能です。

javascript// 基本的なマッチャー
test('基本的なマッチャーの使い方', () => {
  // 同値比較
  expect(2 + 2).toBe(4);
  expect({ name: '田中' }).toEqual({ name: '田中' });

  // 真偽値
  expect(true).toBeTruthy();
  expect(false).toBeFalsy();
  expect(null).toBeNull();
  expect(undefined).toBeUndefined();

  // 数値比較
  expect(2 + 2).toBeGreaterThan(3);
  expect(Math.PI).toBeCloseTo(3.14, 2);

  // 文字列
  expect('Hello World').toMatch(/World/);
  expect('testing').toContain('test');
});

配列とオブジェクトのテスト:

javascripttest('配列とオブジェクトのテスト', () => {
  const users = ['田中', '佐藤', '鈴木'];

  // 配列の検証
  expect(users).toContain('田中');
  expect(users).toHaveLength(3);
  expect(users).toEqual(
    expect.arrayContaining(['田中', '佐藤'])
  );

  const user = {
    id: 1,
    name: '田中太郎',
    profile: {
      age: 30,
      city: '東京',
    },
  };

  // オブジェクトの検証
  expect(user).toHaveProperty('name');
  expect(user).toHaveProperty('profile.age', 30);
  expect(user).toMatchObject({
    name: '田中太郎',
    profile: expect.objectContaining({
      city: '東京',
    }),
  });
});

セットアップとクリーンアップ

テストの前後で必要な処理を実行するためのフック関数を活用します。

javascriptdescribe('データベース操作テスト', () => {
  let database;

  // 全テスト実行前に一度だけ実行
  beforeAll(() => {
    database = new Database();
    database.connect();
  });

  // 全テスト実行後に一度だけ実行
  afterAll(() => {
    database.close();
  });

  // 各テスト実行前に毎回実行
  beforeEach(() => {
    database.clearData();
    database.seedTestData();
  });

  // 各テスト実行後に毎回実行
  afterEach(() => {
    database.clearData();
  });

  test('ユーザーデータを保存できる', () => {
    const user = { name: '田中太郎' };
    const saved = database.save(user);
    expect(saved.id).toBeDefined();
  });
});

実践的なテストケース作成

関数のテスト

実際のビジネスロジックを含む関数のテスト例です。

javascript// テスト対象の関数
function calculateTax(price, taxRate = 0.1) {
  if (price < 0) {
    throw new Error('価格は0以上である必要があります');
  }
  if (taxRate < 0 || taxRate > 1) {
    throw new Error('税率は0から1の間である必要があります');
  }
  return Math.round(price * taxRate);
}
javascriptdescribe('税金計算関数', () => {
  test('正常な価格と税率で税額を計算する', () => {
    const tax = calculateTax(1000, 0.1);
    expect(tax).toBe(100);
  });

  test('税率を省略した場合、デフォルト税率10%が適用される', () => {
    const tax = calculateTax(1000);
    expect(tax).toBe(100);
  });

  test('小数点以下は四捨五入される', () => {
    const tax = calculateTax(333, 0.1);
    expect(tax).toBe(33); // 33.3 → 33
  });

  test('価格が負の値の場合エラーが発生する', () => {
    expect(() => calculateTax(-100)).toThrow(
      '価格は0以上である必要があります'
    );
  });

  test('税率が範囲外の場合エラーが発生する', () => {
    expect(() => calculateTax(1000, 1.5)).toThrow(
      '税率は0から1の間である必要があります'
    );
    expect(() => calculateTax(1000, -0.1)).toThrow(
      '税率は0から1の間である必要があります'
    );
  });
});

非同期処理のテスト

Promise や async/await を使った非同期処理のテスト方法です。

javascript// テスト対象の非同期関数
async function fetchUserData(userId) {
  const response = await fetch(`/api/users/${userId}`);
  if (!response.ok) {
    throw new Error('ユーザーが見つかりません');
  }
  return response.json();
}
javascriptdescribe('ユーザーデータ取得', () => {
  // fetch をモック化
  beforeEach(() => {
    global.fetch = jest.fn();
  });

  afterEach(() => {
    jest.resetAllMocks();
  });

  test('正常にユーザーデータを取得する', async () => {
    const mockUser = { id: 1, name: '田中太郎' };
    fetch.mockResolvedValueOnce({
      ok: true,
      json: async () => mockUser,
    });

    const user = await fetchUserData(1);
    expect(user).toEqual(mockUser);
    expect(fetch).toHaveBeenCalledWith('/api/users/1');
  });

  test('ユーザーが見つからない場合エラーが発生する', async () => {
    fetch.mockResolvedValueOnce({
      ok: false,
    });

    await expect(fetchUserData(999)).rejects.toThrow(
      'ユーザーが見つかりません'
    );
  });

  test('ネットワークエラーの場合エラーが発生する', async () => {
    fetch.mockRejectedValueOnce(new Error('Network Error'));

    await expect(fetchUserData(1)).rejects.toThrow(
      'Network Error'
    );
  });
});

エラーハンドリングのテスト

様々なエラーケースを想定したテストの書き方です。

javascript// テスト対象のクラス
class BankAccount {
  constructor(initialBalance = 0) {
    if (initialBalance < 0) {
      throw new Error(
        '初期残高は0以上である必要があります'
      );
    }
    this.balance = initialBalance;
  }

  withdraw(amount) {
    if (amount <= 0) {
      throw new Error('出金額は0より大きい必要があります');
    }
    if (amount > this.balance) {
      throw new Error('残高不足です');
    }
    this.balance -= amount;
    return this.balance;
  }
}
javascriptdescribe('銀行口座クラス', () => {
  describe('初期化', () => {
    test('正常な初期残高で口座を作成できる', () => {
      const account = new BankAccount(1000);
      expect(account.balance).toBe(1000);
    });

    test('初期残高を省略した場合、残高0で作成される', () => {
      const account = new BankAccount();
      expect(account.balance).toBe(0);
    });

    test('負の初期残高でエラーが発生する', () => {
      expect(() => new BankAccount(-100)).toThrow(
        '初期残高は0以上である必要があります'
      );
    });
  });

  describe('出金処理', () => {
    let account;

    beforeEach(() => {
      account = new BankAccount(1000);
    });

    test('正常に出金できる', () => {
      const newBalance = account.withdraw(300);
      expect(newBalance).toBe(700);
      expect(account.balance).toBe(700);
    });

    test('0以下の出金額でエラーが発生する', () => {
      expect(() => account.withdraw(0)).toThrow(
        '出金額は0より大きい必要があります'
      );
      expect(() => account.withdraw(-100)).toThrow(
        '出金額は0より大きい必要があります'
      );
    });

    test('残高を超える出金でエラーが発生する', () => {
      expect(() => account.withdraw(1500)).toThrow(
        '残高不足です'
      );
    });

    test('残高ちょうどの出金は成功する', () => {
      const newBalance = account.withdraw(1000);
      expect(newBalance).toBe(0);
    });
  });
});

まとめ

Jest 初心者の方に向けて、テスト設計の基本原則について解説しました。重要なポイントを整理すると以下の通りです。

テスト設計の 3 つの基本原則

  1. AAA パターン:準備、実行、検証の 3 段階で構造化
  2. 単一責任:1 つのテストで 1 つのことをテストする
  3. 独立性:テスト間で状態を共有しない

実践のコツ

  • 簡単な関数から始めて、徐々に複雑なケースに挑戦する
  • エラーケースも含めて、様々なシナリオをテストする
  • 非同期処理やモックを適切に活用する

継続的な改善

  • テストコードも通常のコードと同様にリファクタリングを行う
  • テストの実行時間やメンテナンス性を意識する
  • チーム内でテスト品質の基準を共有する

良いテストは、コードの品質向上だけでなく、開発者の自信や開発速度の向上にもつながります。最初は小さく始めて、着実にテストスキルを向上させていきましょう。

継続的にテストを書くことで、バグの少ない高品質なアプリケーション開発が可能になります。ぜひ今回学んだ基本原則を実際のプロジェクトで活用してみてください。

関連リンク