10日で覚えるJestDay 2: テストの構造と基本パターン

Day 2: テストの構造と基本パターン

今日学ぶこと

  • describe によるテストのグループ化
  • testit の違い
  • beforeEach / afterEach によるセットアップとクリーンアップ
  • beforeAll / afterAll の使い方
  • AAA(Arrange-Act-Assert)パターン
  • テストの命名規則とベストプラクティス

describe によるテストのグループ化

Day 1では test() を並べてテストを書きました。しかし、テストが増えてくると整理が必要です。describe を使うとテストをグループ化できます。

// calculator.test.js
const { add, subtract, multiply, divide } = require('./math');

describe('add', () => {
  test('adds two positive numbers', () => {
    expect(add(1, 2)).toBe(3);
  });

  test('adds negative numbers', () => {
    expect(add(-1, -2)).toBe(-3);
  });

  test('adds zero', () => {
    expect(add(5, 0)).toBe(5);
  });
});

describe('divide', () => {
  test('divides two numbers', () => {
    expect(divide(10, 2)).toBe(5);
  });

  test('throws error when dividing by zero', () => {
    expect(() => divide(10, 0)).toThrow('Division by zero');
  });
});

TypeScript版:

// calculator.test.ts
import { add, subtract, multiply, divide } from './math';

describe('add', () => {
  test('adds two positive numbers', () => {
    expect(add(1, 2)).toBe(3);
  });

  test('adds negative numbers', () => {
    expect(add(-1, -2)).toBe(-3);
  });

  test('adds zero', () => {
    expect(add(5, 0)).toBe(5);
  });
});

describe('divide', () => {
  test('divides two numbers', () => {
    expect(divide(10, 2)).toBe(5);
  });

  test('throws error when dividing by zero', () => {
    expect(() => divide(10, 0)).toThrow('Division by zero');
  });
});

実行結果:

PASS  ./calculator.test.js
  add
    ✓ adds two positive numbers (1 ms)
    ✓ adds negative numbers
    ✓ adds zero
  divide
    ✓ divides two numbers
    ✓ throws error when dividing by zero (1 ms)

describe でグループ化すると、出力結果もインデントされて見やすくなります。

describe のネスト

describe はネスト(入れ子)にすることもできます。

describe('Calculator', () => {
  describe('basic operations', () => {
    test('adds numbers', () => {
      expect(add(1, 2)).toBe(3);
    });

    test('subtracts numbers', () => {
      expect(subtract(5, 3)).toBe(2);
    });
  });

  describe('edge cases', () => {
    test('handles negative results', () => {
      expect(subtract(3, 5)).toBe(-2);
    });

    test('handles decimal results', () => {
      expect(divide(10, 3)).toBeCloseTo(3.333);
    });
  });
});
flowchart TB
    subgraph Structure["テスト構造"]
        D1["describe('Calculator')"]
        D2["describe('basic operations')"]
        D3["describe('edge cases')"]
        T1["test('adds numbers')"]
        T2["test('subtracts numbers')"]
        T3["test('handles negative results')"]
        T4["test('handles decimal results')"]
    end
    D1 --> D2
    D1 --> D3
    D2 --> T1
    D2 --> T2
    D3 --> T3
    D3 --> T4
    style D1 fill:#3b82f6,color:#fff
    style D2 fill:#8b5cf6,color:#fff
    style D3 fill:#8b5cf6,color:#fff
    style T1 fill:#22c55e,color:#fff
    style T2 fill:#22c55e,color:#fff
    style T3 fill:#22c55e,color:#fff
    style T4 fill:#22c55e,color:#fff

test と it の違い

Jestでは test()it()全く同じ機能です。どちらを使っても構いません。

// test() を使うスタイル
test('adds 1 + 2 to equal 3', () => {
  expect(add(1, 2)).toBe(3);
});

// it() を使うスタイル
it('adds 1 + 2 to equal 3', () => {
  expect(add(1, 2)).toBe(3);
});

it は英語の文として読みやすくなるのが特徴です。

describe('add function', () => {
  it('returns the sum of two numbers', () => {
    expect(add(1, 2)).toBe(3);
  });

  it('handles negative numbers', () => {
    expect(add(-1, -2)).toBe(-3);
  });
});

出力が "add function > returns the sum of two numbers" のように英語として自然に読めます。

関数 読み方 適した場面
test() 「○○をテストする」 独立したテストケース
it() 「それは○○する」 describe と組み合わせて使う場合

本書では主に test() を使いますが、プロジェクトごとに統一されていればどちらでも問題ありません。


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

良いテストには一貫した構造があります。最も広く使われているのがAAAパターンです。

flowchart LR
    subgraph AAA["AAA パターン"]
        A["Arrange\n準備"]
        B["Act\n実行"]
        C["Assert\n検証"]
    end
    A --> B --> C
    style A fill:#3b82f6,color:#fff
    style B fill:#f59e0b,color:#fff
    style C fill:#22c55e,color:#fff
フェーズ 説明
Arrange(準備) テストに必要なデータやオブジェクトを用意する 入力値の準備、オブジェクトの初期化
Act(実行) テスト対象の処理を実行する 関数の呼び出し
Assert(検証) 結果が期待通りか検証する expect().toBe()

実践例

// user.js
class User {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  greet() {
    return `Hi, I'm ${this.name}!`;
  }

  isAdult() {
    return this.age >= 18;
  }
}

module.exports = User;

TypeScript版:

// user.ts
export class User {
  constructor(
    public name: string,
    public age: number
  ) {}

  greet(): string {
    return `Hi, I'm ${this.name}!`;
  }

  isAdult(): boolean {
    return this.age >= 18;
  }
}
// user.test.js
const User = require('./user');

describe('User', () => {
  test('greet returns a greeting message with the name', () => {
    // Arrange
    const user = new User('Alice', 25);

    // Act
    const result = user.greet();

    // Assert
    expect(result).toBe("Hi, I'm Alice!");
  });

  test('isAdult returns true for users aged 18 or older', () => {
    // Arrange
    const user = new User('Bob', 18);

    // Act
    const result = user.isAdult();

    // Assert
    expect(result).toBe(true);
  });

  test('isAdult returns false for users under 18', () => {
    // Arrange
    const user = new User('Charlie', 17);

    // Act
    const result = user.isAdult();

    // Assert
    expect(result).toBe(false);
  });
});

ヒント: 単純なテストでは、AAA の各フェーズが1行ずつで済むこともあります。その場合、コメントを省略して1行で書いても構いません:expect(add(1, 2)).toBe(3);


beforeEach と afterEach

テストごとに同じセットアップを繰り返す場合、beforeEach を使って共通の準備処理をまとめられます。

describe('User', () => {
  let user;

  beforeEach(() => {
    user = new User('Alice', 25);
  });

  test('greet returns a greeting message', () => {
    expect(user.greet()).toBe("Hi, I'm Alice!");
  });

  test('isAdult returns true', () => {
    expect(user.isAdult()).toBe(true);
  });
});
flowchart TB
    subgraph Lifecycle["テストのライフサイクル"]
        BE["beforeEach()\n各テストの前に実行"]
        T1["test 1"]
        AE1["afterEach()\n各テストの後に実行"]
        BE2["beforeEach()"]
        T2["test 2"]
        AE2["afterEach()"]
    end
    BE --> T1 --> AE1 --> BE2 --> T2 --> AE2
    style BE fill:#3b82f6,color:#fff
    style BE2 fill:#3b82f6,color:#fff
    style T1 fill:#22c55e,color:#fff
    style T2 fill:#22c55e,color:#fff
    style AE1 fill:#f59e0b,color:#fff
    style AE2 fill:#f59e0b,color:#fff

afterEach でクリーンアップ

テスト後にリソースを解放したい場合は afterEach を使います。

describe('Database connection', () => {
  let db;

  beforeEach(() => {
    db = new Database();
    db.connect();
  });

  afterEach(() => {
    db.disconnect();
  });

  test('saves a record', () => {
    db.save({ name: 'Alice' });
    expect(db.count()).toBe(1);
  });

  test('starts with empty database', () => {
    expect(db.count()).toBe(0);
  });
});

beforeAll と afterAll

beforeAll / afterAlldescribe ブロック内で1回だけ実行されます。重い初期化処理に向いています。

describe('API tests', () => {
  let server;

  beforeAll(() => {
    server = startTestServer();
  });

  afterAll(() => {
    server.close();
  });

  test('responds to GET /users', () => {
    // ...
  });

  test('responds to POST /users', () => {
    // ...
  });
});
フック 実行タイミング 使用例
beforeEach 各テストの前 テストデータの作成
afterEach 各テストの後 データのクリーンアップ
beforeAll ブロック内で1回(最初) サーバー起動、DB接続
afterAll ブロック内で1回(最後) サーバー停止、DB切断

ネストした describe でのフックの実行順序

describe('outer', () => {
  beforeEach(() => console.log('outer beforeEach'));
  afterEach(() => console.log('outer afterEach'));

  describe('inner', () => {
    beforeEach(() => console.log('inner beforeEach'));
    afterEach(() => console.log('inner afterEach'));

    test('example', () => {
      console.log('test');
    });
  });
});

実行順序:

outer beforeEach
inner beforeEach
test
inner afterEach
outer afterEach

外側の beforeEach が先に実行され、afterEach は内側から先に実行されます。


テストの命名規則

テスト名は「何をテストしているか」が一目でわかるように書きましょう。

良い命名パターン

// パターン1: 動作を説明する
test('returns the sum of two positive numbers', () => { ... });
test('throws an error when the input is empty', () => { ... });

// パターン2: 条件と結果を明示する
test('isAdult returns true when age is 18', () => { ... });
test('isAdult returns false when age is 17', () => { ... });

// パターン3: describe + it で文として読む
describe('User.isAdult', () => {
  it('returns true for age 18 or older', () => { ... });
  it('returns false for age under 18', () => { ... });
});

避けるべき命名

// ❌ 何をテストしているかわからない
test('test1', () => { ... });
test('it works', () => { ... });

// ❌ 実装の詳細に依存
test('calls Math.max internally', () => { ... });
ルール 良い例 悪い例
動作を説明する returns null for invalid input test case 3
条件を含める throws error when array is empty error test
簡潔にする formats date as YYYY-MM-DD should correctly format the given date object into YYYY-MM-DD string format

test.each による パラメータ化テスト

同じロジックを異なる入力でテストする場合、test.each を使うとテストの重複を減らせます。

const { add } = require('./math');

describe('add', () => {
  test.each([
    [1, 2, 3],
    [0, 0, 0],
    [-1, 1, 0],
    [100, 200, 300],
  ])('add(%i, %i) = %i', (a, b, expected) => {
    expect(add(a, b)).toBe(expected);
  });
});

TypeScript版:

import { add } from './math';

describe('add', () => {
  test.each([
    [1, 2, 3],
    [0, 0, 0],
    [-1, 1, 0],
    [100, 200, 300],
  ])('add(%i, %i) = %i', (a, b, expected) => {
    expect(add(a, b)).toBe(expected);
  });
});

実行結果:

PASS  ./math.test.js
  add
    ✓ add(1, 2) = 3
    ✓ add(0, 0) = 0
    ✓ add(-1, 1) = 0
    ✓ add(100, 200) = 300

オブジェクト形式の test.each

より読みやすい形式として、オブジェクトの配列も使えます。

describe('isAdult', () => {
  test.each([
    { age: 18, expected: true, label: 'exactly 18' },
    { age: 25, expected: true, label: 'over 18' },
    { age: 17, expected: false, label: 'under 18' },
    { age: 0, expected: false, label: 'zero' },
  ])('returns $expected when age is $age ($label)', ({ age, expected }) => {
    const user = new User('Test', age);
    expect(user.isAdult()).toBe(expected);
  });
});

テストのスキップとフォーカス

開発中に特定のテストだけ実行したい場合や、一時的にスキップしたい場合のテクニックです。

// このテストだけを実行(他は無視)
test.only('this test runs', () => {
  expect(1 + 1).toBe(2);
});

// このテストをスキップ
test.skip('this test is skipped', () => {
  expect(1 + 1).toBe(2);
});

// describe にも使える
describe.only('only this group runs', () => {
  test('test 1', () => { ... });
});

describe.skip('this group is skipped', () => {
  test('test 2', () => { ... });
});
修飾子 効果 使い場面
.only そのテスト/グループだけを実行 デバッグ中、特定のテストに集中
.skip そのテスト/グループをスキップ 一時的に無効化、未実装のテスト

注意: .only.skip はコミットに含めないよう注意しましょう。ESLintの no-only-tests ルールを使うと、.only がコードに残ったままコミットされるのを防げます。


まとめ

概念 説明
describe テストをグループ化する。ネストも可能
test / it テストケースを定義する(同じ機能)
AAAパターン Arrange → Act → Assert の3段階でテストを構造化
beforeEach / afterEach 各テスト前後に実行される共通処理
beforeAll / afterAll ブロック内で1回だけ実行される処理
test.each パラメータ化テストで重複を削減
.only / .skip テストのフォーカスとスキップ

重要ポイント

  1. describe でテストを論理的にグループ化し、読みやすい構造を作る
  2. AAAパターンに従うと、テストの意図が明確になる
  3. beforeEach は便利だが、使いすぎるとテストの可読性が下がることがある
  4. テスト名は「何をテストしているか」が一目でわかるように書く

練習問題

問題1: 基本

以下の ShoppingCart クラスのテストを describe でグループ化して書いてください。

class ShoppingCart {
  constructor() {
    this.items = [];
  }

  addItem(name, price) {
    this.items.push({ name, price });
  }

  getTotal() {
    return this.items.reduce((sum, item) => sum + item.price, 0);
  }

  getItemCount() {
    return this.items.length;
  }

  clear() {
    this.items = [];
  }
}

問題2: 応用

上記の ShoppingCart テストで beforeEach を使い、各テストの前にカートを初期化するように書き直してください。

チャレンジ問題

test.each を使って、以下の fizzbuzz 関数を複数の入力でテストしてください。

function fizzbuzz(n) {
  if (n % 15 === 0) return 'FizzBuzz';
  if (n % 3 === 0) return 'Fizz';
  if (n % 5 === 0) return 'Buzz';
  return String(n);
}

参考リンク


次回予告: Day 3では「マッチャーをマスターする」について学びます。toBe 以外にも toEqualtoContaintoThrow など、Jestが提供する豊富なマッチャーを使いこなしましょう!