Day 2: テストの構造と基本パターン
今日学ぶこと
describeによるテストのグループ化testとitの違い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 / afterAll は describe ブロック内で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 |
テストのフォーカスとスキップ |
重要ポイント
describeでテストを論理的にグループ化し、読みやすい構造を作る- AAAパターンに従うと、テストの意図が明確になる
beforeEachは便利だが、使いすぎるとテストの可読性が下がることがある- テスト名は「何をテストしているか」が一目でわかるように書く
練習問題
問題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 以外にも toEqual、toContain、toThrow など、Jestが提供する豊富なマッチャーを使いこなしましょう!