🐥
テストを書くときにハマりがちなポイントまとめ(データ準備・assert・整理の仕方)
※本記事は ChatGPT による生成コンテンツをもとに、筆者が加筆・修正を行ったものです。
1) データ準備が複雑になる問題
痛みの正体
- 「全部指定」地獄:テストごとに全フィールド埋める
- コピペ肥大化:似た準備が散らばる
- 意図が読めない:この値は意味があるの?ただのダミー?
解決パターン
-
Test Data Builder / Object Mother
- “最小限の正当データ”にデフォルトを持つビルダーを用意し、差分だけ上書きする
-
シナリオ関数(Given系ヘルパ)
- 「有効期限切れのユーザー」「在庫ゼロの商品」など意味で準備する
-
階層化したファクトリ
-
minimal()
/typical()
/edge()
の3段構え
-
// Test Data Builder 例(PHPUnit)
final class UserBuilder {
private array $attrs = [
'name' => 'Alice',
'email' => 'alice@example.com',
'status' => 'active',
'expires_at' => null,
];
public static function typical(): self { return new self(); }
public function expired(): self { $this->attrs['expires_at'] = new DateTime('-1 day'); return $this; }
public function with(array $overrides): self { $this->attrs = array_replace($this->attrs, $overrides); return $this; }
public function build(): User { return User::fromArray($this->attrs); }
}
// 使う側:差分だけ表現
$user = UserBuilder::typical()->expired()->with(['name' => 'Bob'])->build();
ポイント:「何を検証したいか」以外はデフォルトに寄せる。差分=テスト意図になる。
2) assert の書き方が分からなくなる
迷う理由
- 1テストで状態+イベント+副作用を全部見ようとする
- 期待の表現が冗長で、失敗時に何がダメかわかりにくい
パターン
- **AAA(Arrange-Act-Assert)**で段落分け
- 1つの振る舞いにフォーカス(ただし複数assertはOK。ただし“同じ振る舞い”に属していること)
- カスタムアサート/Constraintで意図を名前にする
// AAA & カスタムアサートの例
public function test_activate_sets_status_and_emits_event(): void
{
// Arrange
$user = UserBuilder::typical()->build();
// Act
$events = captureEvents(fn() => $user->activate());
// Assert(振る舞いは「有効化」1つ)
$this->assertUserActive($user); // 名前が意図を語る
$this->assertEventEmitted($events, UserActivated::class);
}
// テスト専用アサート
private function assertUserActive(User $u): void {
$this->assertSame('active', $u->status, 'User should become active');
}
- **テーブル駆動(データプロバイダ)**で分岐網羅を短く:
/** @dataProvider expiresProvider */
public function test_isExpired(DateTimeInterface $at, bool $expected): void {
$user = UserBuilder::typical()->with(['expires_at' => new DateTime('2025-09-01')])->build();
$this->assertSame($expected, $user->isExpired($at));
}
public static function expiresProvider(): array {
return [
'before' => [new DateTime('2025-08-31'), false],
'equal' => [new DateTime('2025-09-01'), true],
'after' => [new DateTime('2025-09-02'), true],
];
}
失敗時に何がズレたか一目で分かるメッセージを意識(
assertSame
の第3引数やカスタムアサート)。
3) どう切り出して整理すべきか
ガイド
-
境界で切る(ポート/アダプタ)
外部API・DB・キューはポート(インターフェース)を介して呼ぶ。ユニットテストはFakeで差し替え、統合は別テストで。 -
ヘルパはテスト階層内に閉じ込める
tests/_support
的な場所にBuilder/シナリオ関数を集約。アプリ本体に漏らさない。 -
命名で意図を伝える
test_期待する結果_前提条件
かtest_doSomething_whenCondition_thenOutcome
-
スナップショットは最小限
フレーク多い。構造が安定しないものは専用アサートへ分解。
依存の扱い(モック病を避ける)
- 基本は状態検証(State-based):戻り値や保存後の状態を見る
- 振る舞い検証(Behavior-based)は境界に限定:外部サービス呼び出し回数や引数の検証など
- 契約テスト:外部クライアントは**契約(インターフェース)**の形で別枠で担保
小さなチェックリスト
- データは Builder で最小+差分表現にしたか
- テストは AAA で段落が分かれているか
- 1つの振る舞いにフォーカスしているか
- 失敗メッセージが読めばわかるか(カスタムアサート活用)
- 分岐は データプロバイダで網羅したか
- 外部依存は インターフェース化 + Fake/Mock に切り出したか
- テスト専用ヘルパは tests配下に閉じているか
Discussion