🐥

テストを書くときにハマりがちなポイントまとめ(データ準備・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