去年の秋、社内で別々に開発されていた3つのサービスを、1つの予約システムとしてつなぐプロジェクトに入りました。在庫管理はAチーム、決済はBチーム、通知まわりは外部の委託先。それぞれのサービスは単体テストも自チームの結合テストも通っていて、リリース判定会議の資料には緑のチェックがずらっと並んでいました。
それなのに、統合環境で初めて予約を通しで流した日、1件も最後までたどり着きませんでした。
この記事は、そのとき何が起きていたのか、そして最終的にどういうテストの組み立て方に落ち着いたのかを残しておくためのメモです。
前提:誰も間違っていなかった
先に書いておくと、どのチームの実装もそれぞれの仕様書どおりでした。問題は仕様書と仕様書の「間」にありました。
具体的にはこんな感じです。
- 在庫サービスは予約IDを数値(int64)で返していた
- 決済サービスは予約IDを文字列で受け取り、ゼロ埋め12桁を期待していた
- 通知サービスは決済完了イベントの
reservation_idを見てテンプレートを引くが、決済チームの最新版ではフィールド名がreservationIdに変わっていた - 在庫側のタイムアウトは3秒、決済側は内部リトライ込みで最大5秒かかることがあった
どれも、各チームのテストでは出てこない種類のズレです。各チームのテストは、自分のサービスと「自分たちが想定した相手の振る舞い」、つまりモックとの間で閉じていたからです。
最初の2週間は手作業だった
最初は完全に人力でした。統合環境に3サービスを立てて、Postmanのコレクションで予約フローを1ステップずつ叩き、ログを3画面並べて追いかける。Slackのスレッドには「今どこで止まってます?」「決済のログにリクエスト来てないです」「在庫側は200返してますけど」というやり取りが毎日何十件も流れていました。
1件のバグを特定するのに半日かかることもありました。で、原因がわかってみるとヘッダー名の大文字小文字だったりする。あの週は本当に疲れました。
ここでようやく、各チームがそれぞれ持っている integration testing(結合テスト) とは別に、システム全体をつないだ状態で業務フローを流すテストが要る、という合意がチーム間でできました。今思えば最初から分かっていたことなのですが、各チームの予算とスケジュールが別々だったので、「誰がそれをやるのか」が決まらないまま統合日を迎えてしまったわけです。
2種類のテストの線引きで揉めた
合意はできたものの、そこから先で1週間ほど揉めました。論点は単純で、「どこまでが各チームの責任で、どこからが共通のテストの責任か」です。
決済チームからは「相手のフィールド名が変わったかどうかまでうちのテストで見るのはおかしい」という意見が出ました。通知の委託先からは「イベントの形が変わるなら事前に連絡がほしい、テストで気づくのは遅すぎる」と言われました。どちらも筋は通っています。
最終的に決めた線引きはこうです。
結合テストは、1つのチームが責任を持つ範囲で、自分のサービスと直接の依存先(DB、キュー、相手サービスとの契約)をつないで確かめるもの。各チームのCIで回ります。
システム結合テストは、複数チームのサービスを実際にデプロイした状態で、業務として意味のあるフローを端から端まで流すもの。予約する、決済する、通知が飛ぶ、キャンセルする、返金される。こちらは共通の統合環境で、どのチームにも属さない専用のパイプラインで回します。
言葉の定義そのものは教科書的な話ですが、system integration testing(システム結合テスト) で何を確認するのかを文書にしてチーム間で合意しておいたのが、結果的に一番効きました。「それはそっちのテストで見るべきでは」という押し付け合いがほぼなくなったからです。
システム結合テストの中身
シナリオは業務側と一緒に決めた
最初はエンジニアだけでシナリオを書こうとして、細かい異常系ばかり30本も並べてしまいました。業務担当の方に見てもらったら、「それより、同じ枠に2人が同時に予約したときにどうなるかを見てほしい」と言われて、ああそうか、となりました。現場で本当に困るのはそっちなんですよね。
最終的にシナリオは12本に絞りました。正常系の予約から通知まで、同時予約、決済失敗時の在庫戻し、部分キャンセル、返金、それと日付をまたぐ夜間バッチ中の予約あたりです。
環境とテストデータ
統合環境は毎朝リセットして、固定のシードデータを入れ直します。外部の決済代行だけはサンドボックスを使っていますが、レスポンスが妙に遅い日があるので、テスト側のタイムアウトは少し長めの15秒にしています。
一番面倒だったのはテストデータの準備でした。各チームが自分のDBにどんな前提データを期待しているのかが、どこにも書かれていなかったからです。手で揃えるのは途中で諦めて、本番に近い環境で実際の通信を記録し、そこからテストケースとモックを起こすやり方も試しました。Keployのように実トラフィックを記録してテストとモックを生成するツールもありますし、自前でHTTPの記録と再生の仕組みを作っているチームもあります。うちは一部のサービスだけ記録ベースに切り替えました。
落ちたときに誰が見るか
地味ですが、運用上いちばん大事だったのがここです。システム結合テストが落ちたときに一次対応するチームを決めていないと、全員が「うちじゃなさそう」と思って放置されます。実際、最初の1週間はそうなりました。
うちでは「落ちたシナリオで最初に失敗したステップのサービスを持つチームが一次対応」というルールにしました。全サービスでトレースIDを引き回すようにしたので、どのステップで落ちたかはダッシュボードを見ればすぐわかります。
やってみてわかったこと
単体テストと各チームの結合テストが全部通っていても、システムとしては動かない。文章にすると当たり前ですが、今回の件で一番身にしみたのはそこでした。
もう一つは、システム結合テストは「最後にまとめてやるもの」ではなく、統合の初日から少しずつ育てるものだということです。シナリオが1本でもあれば、予約IDの型の不一致は初日に見つかっていたはずです。
今は3チームとも、自分たちのAPIに変更を入れるときは統合環境のシステム結合テストが通ることをマージ条件にしています。最初は反発もありましたが、例の2週間を経験した人が多いので、思ったよりすんなり受け入れられました。