GoQSystem Tech Blog

GoQSystemのテックブログ

品質保証から仕様定義へ、テストの捉え方を変える

GoQSystem Advent Calendar 2025」12日目

こんにちは、GoQSystemでバックエンドエンジニアをしている松澤です。

個人的にここ半年、テストをしっかり書くことを意識して取り組んできました。 理由は特になく、「メリットしか聞かないから書いた方がいいのだろう」という程度の意識でしたが、半年経って巷で謳われているメリットを実感できました。 そこで感じたことを共有したいと思います。

テストを後回しにしていた頃

以前はテストの重要性は理解していたものの、以下のような理由でテストを後回しにして結局書かないということが多々ありました。

  • 「リリースが先だ、テストは後で書こう」
  • 「時間がない、余裕ができたら書こう」
  • 「動いているから大丈夫、先に新機能をリリースしよう」

振り返ると、当時の私はテストを「品質保証だけ」の文脈で捉えていたのだと思います。人力の検証でそこは保証できていると考えていたため、リリースという目の前の締め切りに比べると、テストを書くプライオリティは下がっていました。

そこで、テストを「品質保証だけ」ではなく、「あるべき振る舞いを定義する工程の一つ」として捉えるようにしました。 捉え方を変えてからは、「とりあえず書いている」状態から「明確な意図を持って書く」状態で開発を進められるようになりました。

テストで「あるべき振る舞い」を定義する

ソフトウェアという製品のリリースを行う際、私は製造業出身ということもあり、「当たり前品質に達しているか?」という観点で考えることが多いです。 (注:当たり前品質 = 最低限満たすべき品質基準のこと)

テストを書き始めて、ソフトウェア開発の文脈だと、「あるべき振る舞いができているか?」と言い換えることができると考えました。

製品をリリースできる最低限の品質とは、「あるべき振る舞い」が定義され、それを達成している状態です。

テストで振る舞いを定義すれば、仕様の明文化と動作保証を同時に達成できます。

つまり、テストを品質保証だけではなく、仕様を明文化し、チームで合意を取るためのツールだと考えれば、テストを書くプライオリティは自然と上がります。

また、テストで振る舞いを定義することで、ドキュメントに記載のない仕様にも気づくことができます。

曖昧さを許さない

テストを書くという行為には、曖昧さを許さないという効果があります。

以下のことを明確にしなければならないからです。

  • 成功とは何か?
  • 失敗とは何か?
  • 境界値はどこか?
  • 例外が発生したらどうするか?

特に私が効果を感じたのは、例外の扱いです。

テストを書かない状態では、「成功パス」だけを考えがちです。しかしテストを書こうとすると、「失敗した場合はどうする?」という問いを強制されます。

例えば、以下のようなケースです。

  • APIリクエストがタイムアウトしたら?
  • DBへの書き込みが失敗したら?
  • 外部サービスが503を返したら?

これらの問いに答えることで、「あるべき振る舞い」が明確になります。

私の経験ですが、テストを書くことでこの思考が習慣化し、設計段階で例外処理を考慮できるようになりました。後工程のためだけでなく、自分の成長にも繋がるので、書く価値があると感じています。

具体例:キャンセル処理で仕様を定義する

例えば、以下のようなキャンセル処理があったとします。

  1. キャンセルリクエストを外部APIに送信する
  2. APIから結果を受け取る
  3. 成功したものだけDBを更新する

ここで問題になるのは、「2件中1件失敗したら、どう振る舞うべきか?」という点です。

  • 1件でも失敗したら全体を失敗とするのか?
  • 成功した分は処理し、失敗した分だけエラーとするのか?

この判断を曖昧なまま実装することはできません。テストを書くことで決断を強制されます。

仮に「成功分は処理する」という仕様に決めたとすると、以下のようになります。

test('2件のキャンセルリクエストのうち1件が失敗した場合、成功した1件のみDBを更新する', function () {
    // Given: 2件のキャンセル対象が存在する
    $booking1 = Booking::factory()->create(['status' => 'confirmed']);
    $booking2 = Booking::factory()->create(['status' => 'confirmed']);

    // Given: 外部APIが1件目は成功、2件目は失敗を返すようモック設定
    Http::fake([
        'api.example.com/cancel/*' => Http::sequence()
            ->push(['success' => true], 200)   // 1件目: 成功
            ->push(['success' => false], 400), // 2件目: 失敗
    ]);

    // When: キャンセル処理を実行
    $result = (new CancelService())->cancelMultiple([$booking1->id, $booking2->id]);

    // Then: 1件目はキャンセル済み、2件目は確定済みのまま
    expect($booking1->fresh()->status)->toBe('cancelled');
    expect($booking2->fresh()->status)->toBe('confirmed');

    // Then: 結果に成功1件、失敗1件が含まれる
    expect($result['success'])->toHaveCount(1);
    expect($result['failed'])->toHaveCount(1);
});

このテストを読めば、以下のことが明確になります。

  • 部分的な成功を許容する仕様である
  • 失敗したリクエストは、DBに影響を与えない
  • 結果には、成功と失敗が分けて返される

このようなテストが存在することで、未来の開発者(または未来の自分)は、「部分的な成功を許容する」という仕様を正確に理解できます。仕様書は古くなることがありますが、テストは常に最新の振る舞いを表しています。

おわりに

半年間「テストを書く」ということに取り組んだ結果、テストの捉え方が大きく変わりました。

テストを「品質保証のためのもの」ではなく、「あるべき振る舞いを定義する工程」として捉えることで、仕様の明文化と動作保証を同時に達成できます。 また、「テストを書く」という行為は成功・失敗・例外を明確にすることを強制するため、設計段階から例外処理を考慮する習慣が身につきます。

「メリットしか聞かないから書いた方がいい」という曖昧な意識から始めましたが、半年間書き続けることで、そのメリットを実感できました。この記事が、テストを書く動機付けの一つになれば幸いです。