API自動化が同じ4つの地点で行き詰まる理由

どのチームも始まり方は同じです。誰かが1つのエンドポイントに対してリクエストを書き、ステータスコードを検証し、CIに組み込みます。それは動きます。やがてテストスイートが大きくなると、どのチームにもほぼ同じ順序で4つの問題が現れます。

トークンは期限切れになるため、朝は通っていたスイートが昼には失敗します。レスポンスには次のリクエストが必要とする値が含まれるため、テストはその値をハードコードするか、さもなければ壊れます。何かが作成されるまで呼び出せないエンドポイントもあるため、実行順序が意味を持ち始めますが、その順序はどこにも書き残されていません。そして実行のたびにレコードが残るため、10回目の実行は1回目とは違う振る舞いをします。

どれも特殊な話ではありません。実際のバックエンドとはそういうものです。長く使われ続けるスイートと、四半期で削除されるスイートを分けるのは、この4つを設計段階で織り込んでいたか、それとも後から発見したかの違いです。

サービスが実際に公開しているものから始める

その前に必要なのは棚卸しです。記憶を頼りにテストを書けば、たまたま思い出したエンドポイントだけを網羅したスイートができあがります。そしてそれは、たいてい既に正しく動いていたエンドポイントです。

実像をつかむには2つの入力が必要です。OpenAPIファイルや要件定義書といった仕様書は、何が存在するはずかを教えてくれます。稼働中のサービスに対する探索は、何が実際に存在するかを教えてくれます。この2つのギャップはそれ自体が興味深く、カバレッジを判断する出発点としては白紙よりも優れています。TestSpriteではこの最初のステップがAPI Discoveryにあたり、PRD、APIドキュメント、または実際の呼び出しの観測を入力として受け付けます。

そこからは、すべてを受け入れるのではなく、カバーする価値のあるエンドポイントを選びます。管理用や内部用のルートまで全部入った生成プランはノイズであり、ノイズはスイートが読まれなくなる原因です。

問題その1:認証(Auto-Authentication)

素朴なやり方は、トークンを環境変数に入れておき、失敗に誰かが気づいたら手で更新するというものです。これは、その仕組みを知っている人が休暇に入るまでは機能します。

本来望ましいのは、認証情報が個々のテストではなく環境に属し、ログインが実行の一部として行われることです。TestSpriteではこれをAuto-Authenticationと呼び、ログインフローを一度設定しておけば、実行のたびに新しいトークンが取得されます。ここでは、聞こえ以上に重要な原則が2つあります。

  • 認証情報をテストから切り離します。 パスワードを含むテストは、共有もできず、別の環境に向けることもできず、コミットすることもできません。テストケースが表すべきは「サインイン済みのユーザーとして」であって、「user@example.com としてこのパスワードで」ではありません。

  • サービスが許す場合は、実行ごとに1回だけ認証します。 ログインにレート制限のあるサービスは、ケースごとにサインインするスイートを拒否し始めますし、その拒否はプロダクトの不具合とまったく同じに見えます。この種の誤った失敗を切り分けるコストは、最初から設計で避けておくコストよりも高くつきます。

問題その2:実行時にしか存在しない値(Dynamic Variables)

作成の呼び出しは識別子を返します。更新と削除の呼び出しはその識別子を必要とします。識別子をテストに貼り付けてしまえば、そのテストが有効なのは一度きりです。

解決策は、レスポンスから値を取り出して後続で参照することです。そうすればスイートは値を決め打ちせず、データを次へ引き継いでいけます。実際のところ、テストが指定するのは DELETE /orders/8412ではありません。この実行が作成した注文を、その識別子が何になったかにかかわらず削除する、と指定します。

文字にすると当たり前に聞こえますが、先月まで通っていたスイートが今日失敗する理由としては、これが最も多いものです。TestSpriteではこの仕組みにDynamic Variablesという名前が付いており、あるレスポンスから値を取り出して次のリクエストに差し込みます。その配線は手作業で維持するのではなく、自動的に導き出されます。

問題その3:互いに依存し合うエンドポイント(Dependency Chains)

存在しないカートに商品を追加することはできません。商品の入っていないカートで決済を完了することもできません。実際のAPIカバレッジは呼び出しのフラットな一覧ではなく、グラフです。

このグラフが暗黙のままだと、2つの問題が起きます。あるマシンではたまたま通るが別のマシンでは通らない順序でテストが実行されること、そして早い段階の1件の失敗が、それぞれ別の不具合に見える20件の後続の失敗を生むことです。

依存関係を明示すれば、どちらも解決します。各ケースが必要とするものと生み出すものを宣言し、実行側はそこから安全な順序を組み立てます。そして何かが失敗したときに読むことになるのは、20行の赤ではなく、1件の失敗と19件のスキップです。これが、1つのルートを個別にカバーするEndpoint Testsと、あるレスポンスが次の入力になる複数ステップの連鎖であるIntegration Testsの違いです。

問題その4:後に残るデータ(Auto-Cleanup)

レコードを作成するだけで削除しないスイートは、放っておいても自分のペースで劣化していきます。一覧は長くなり、ページネーションのアサーションはずれ、一意制約が引っかかり始め、最後には金曜日に誰かが手作業でテーブルを空にすることになります。

クリーンアップは実行の一部であり、その実行が作成したリソースに紐づいている必要があります。たとえば名前が test-で始まるものをすべて削除するといったパターンによる後片付けは、2つの実行が重なるか、業務側の誰かが運悪くその名前でレコードを作るまでは機能します。Auto-Cleanupは、実行が終わった時点でその実行が作成したレコードを削除します。これが並行実行にも耐えるやり方です。

機能するAPI自動化の全体像

この4つを組み合わせると、1年使い続けても崩れない構成はこうなります。

最初に一度だけプロジェクトに仕様書を渡し、探索によって稼働中のエンドポイントを洗い出します。カバーする価値のあるエンドポイントを選びます。初日から完全網羅を目指さないようにします。認証情報はテストごとではなく環境ごとに設定します。
ケースごとに次のステップが必要とする値は、ハードコードせずに取り出します。そのケースが必要とするものと生み出すものを宣言します。ステータスコードだけでなく、振る舞いを検証します。誤ったボディを返す200も、やはり不具合です。
実行ごとに認証は一度だけ行い、セッションを使い回します。実行順序は依存関係のグラフに決めさせます。実行が終わったら、その実行が作成したものだけを削除します。

パイプラインに組み込む

誰かが思い出したときにだけ動くAPIスイートは、マージ前ではなくリリース前にしか動きません。これでは順番が逆です。組み込み方は2通りあり、どちらを選ぶかは、トリガーを誰に持たせたいかという問題です。

選ぶ前に知っておく価値があるのは、入り口が2つあり、それぞれ解き方が違うということです。Webポータルは生成と維持を任せる経路で、探索、依存関係の配線、クリーンアップが自動的に導き出されます。CLIは自分のコードを持ち込む経路です。バックエンドのテストは自分で書くPythonであり、各テストが必要とするものと生み出すものは自分で宣言し、後片付けもそれと分かるように印を付けたテストとして書きます。問題は同じ4つで、分担が違うだけです。スイートを自分たちのリポジトリに置きたいチームは、意識して後者を選びます。

経路は2つあり、選択の基準は設定をどこに置きたいかです。

経路は2つあり、選択の基準は結局のところパイプラインを誰が所有するかです。ダッシュボードから GitHub App を接続する方法なら、リポジトリに変更を加える必要は一切ありません。ビルドが既に出力しているデプロイに反応するからです。一方、 GitHub Actions のステップを追加すれば、チェックはリポジトリの中に置かれ、ビルドの他の部分と同じようにレビューされます。現在のコマンドは CLIリポジトリにあります。

正直にお伝えする2つの限界

これは機能面の検証です。生成されるプランには境界値と認可のケースも含まれるため、範囲外の値が拒否されるか、未認証のリクエストが弾かれるかは実行結果からわかります。ただしそれは負荷テストや脆弱性スキャンとは別物です。持続的な同時アクセス下でサービスがどう振る舞うかはわかりませんし、スキャナーでもありません。それらは別の道具であり、同じものとして扱えば午後の時間を無駄にするだけです。

また、仕様もなく一貫した規約もないAPIを救ってくれるわけでもありません。探索はエンドポイントを列挙できますが、それぞれにとって「正しい」とは何かを決めるのは依然として判断であり、それはチームの誰かが行わなければなりません。手作業でスイートを書くよりは小さな仕事ですが、ゼロにはなりません。

TestSpriteは4つの問題をどう扱うか

このページで挙げた4つの失敗要因はAPIテストに付随的なものではないため、利用者側のインフラに委ねるのではなく、製品側の機能として扱っています。Auto-Authenticationはログインフローを一度受け取り、実行のたびに新しいトークンを取得します。Dynamic Variablesはあるレスポンスの値を次のリクエストへ引き継ぎます。Dependency Chainsは各ケースが必要とするものと生み出すものから安全な実行順序を導き出します。Auto-Cleanupは実行が作成したレコードだけを削除します。これが並列実行にも耐えるやり方です。

カバレッジの出どころも違います。API Discoveryは稼働中のサービスが実際に公開しているものを列挙し、plan generation は機能、スキーマ、認可、エラー処理、境界値といったカテゴリにまたがるケースを提案します。それを普通の言葉で調整していきます。出来上がるものには、スプリント終盤には誰も書かない異常系のケースも含まれます。

その結果として残るのは、APIが変わっても動き続けるスイートです。しかも、一人しか理解していないconftestやサポート用ディレクトリを抱えずに済みます。負荷容量と脆弱性スキャンは、引き続き別の道具です。

始めるにはOpenAPI仕様が必要ですか?

あると役立ちますが、必須ではありません。要件定義書と、稼働中のサービスに対する探索があれば、実用的なプランはできます。仕様書が主に効いてくるのは、最初に生成されるケース群の質です。

保存したリクエストのコレクションと何が違うのですか?

コレクションが保存するのはリクエストです。この記事で扱った4つの問題は、リクエストとリクエストのあいだの状態、つまりセッション、取り出した値、順序、後片付けに関するものです。コレクションでも十分なスクリプトを書けば対応できますが、それこそが多くのチームが避けようとしている保守の負担です。

外部から到達できないAPIもテストできますか?

どちら側の話かを確かめてください。CLIは自分のマシンで動いているアプリにトンネルできるため、localhost上のフロントエンドは対象になります。このローカルモードはフロントエンド専用なので、インターネットから到達できないバックエンドサービスについては、APIの実行に到達可能なURLが依然として必要です。展開を計画する前に、自社のステージング構成で確認しておく価値があります。

多要素認証付きのログインの背後にあるAPIはどうですか?

第2要素を外した専用のテストアカウントを、テスト環境に限定して用意してください。実際の第2要素を自動化することも可能ですが、たいていの場合は見合わないほど壊れやすくなります。

最初のスイートはいくつのエンドポイントをカバーすべきですか?

書きやすいエンドポイントではなく、リリース前に不安になるフローをカバーしてください。プルリクエストのたびに走る意味のある10件のケースは、誰も読まない200件に勝ります。

この4つにはそれぞれ、 APIテストのドキュメント に個別のページがあります。考え方ではなく仕組みのほうを知りたい場合はそちらをご覧ください。

短くまとめると

セッション、状態、順序、後片付けは初日から設計に織り込みます。

動いていたAPIスイートを放棄されたスイートに変えるのは、この4つです。これらに明示的に対処し、スイートは手作業ではなくパイプラインから実行し、対象範囲は本当に心配なフローに絞ってください。