テックリードとして2つのプロジェクトのテスト自動化を推進しています(本業)。
この記事では、プロジェクトでの経験を踏まえ、APIテストの位置づけ、システム上のスコープ、2つの方式について説明します。
この記事は、テストピラミッドの考え方を採用し、以下のような方針でテスト自動化を進めることが前提です。
上記の前提を読んでも理解できなかった方は、以下の記事を先に読んでいただけますと幸いです。
APIテストは、Webサービス単体の外部仕様のうち、システム間のインターフェースであるAPIを検証するためのものである。

テストピラミッドの考え方に従い、以下のことが重要である。
一般に、Webサービスは複数のAPIサーバーやデータベースから構成される。
このとき、システムをどのように分割してAPIテストを行うかが重要である。
例えば、以下のようなシステム構成で、APIテストを行いたいとする。
このとき、システムをどのように分割してAPIテストを行うのか、意識的に決める必要がある。
例えば、各APIサーバーやデータベースをそれぞれ独立したAPIとみなし、個別にAPIテストを行うことも可能である。この場合、以下のようにシステムが分割され、APIテストが行われる。

もう一つの極端な方法は、全てを接続して1つのサービスとみなし、APIテストを行うことである。

おすすめは、以下のようにAPIサーバーとデータベースを1つの(マイクロ)サービスと捉えてAPIテストすることである。つまり、別APIサーバーは含めないが、データベースは含める。

別APIサーバーを含めてしまった場合、APIサーバー間をまたいだ、より粒度の大きいテストとなってしまい、以下のようなデメリットがある。
これは、テストピラミッドの、「粒度の大きいテストに頼らず、粒度の小さいテストを品質の拠り所にするという」という原則にも通ずる。
加えて、APIテスト特有の課題もある。
したがって、API間はそれぞれ別の(マイクロ)サービスと捉え、個別にAPIテストすべきである。
一方で、APIサーバーとデータベースをまとめて1つの(マイクロ)サービスとみなし、その外部仕様をAPIテストすることは理にかなっている。
データベースは、ロジックを持たず、単独での品質を保証すべきAPIではない。そのため、APIサーバーとデータベースを結合してもケース数が増えることはない。
もし仮に、データベースをスタブしてAPIサーバーだけをAPIテストすると、データベースを独立したサービスとみなし、データベースに対してもAPIテストを行う必要が出てきてしまう。これは、データベースが想定したクエリを受け取れるかといったテストになるだろう。
しかし、データベースはロジックを持たず、APIサーバーと密に結合している。そのため、APIサーバーとデータベースを個別にAPIテストしようとすると、以下のようになってしまう。

一方のAPIサーバーとAPIサーバーの間は、それぞれが一定の責務を持ち、疎結合性を意識したAPI(例えばRESTやGraphQLのような)によって連携している。B APIサーバー内でメールの文面を変えたり、B APIサーバー用のデータベースへの読み書きを最適化しても、B APIサーバーの外部仕様に影響が出てAPIテストの後方互換性を破壊することは少ない。

これは、疎結合・高凝集なコンポーネントの間は分割してテストするべきだが、密結合なコンポーネント間は分割してテストするデメリットのほうが大きいことの例である。
以上のことから、APIサーバーとデータベースは1つの(マイクロ)サービスとみなし、まとめてAPIテストを行うべきである。
APIの振る舞いは、複数のレイヤから実現されており、以下図のようにモデル化できる。
クライアントからのHTTP等によるAPIリクエストは、

APIテストの信頼度は、以下のように決まる。
一方で、APIテストにおいても当然、開発生産性が求められる。
レイヤは左にいけばいくほどテストがしづらいため、より多くのレイヤをカバーすれば、手間が増して開発生産性は低下する。
したがって、プロダクトの特性を踏まえて、このトレードオフを調整する必要がある。
今回は、2つのAPIテスト方式を説明する。
特に決まった名前はないように思われるが、便宜上この記事では以下のように命名する。
この記事における推奨は、より開発生産性が高いテストコード方式である。そのため、サーバー起動方式のデメリットや、テストコード方式のメリットを強調する形で説明する。
プロダクション同様にサーバーを起動し、実際にHTTP等のリクエストを送り、レスポンスやサーバーに生成されたデータを検証する方式である。

この方式のメリットは、より確実にサーバーが起動した状態でAPIが正しく振る舞うことを検証できることである。
このテスト方式では、以下のようなレイヤをテストすることができる。ポイントだけ以下に述べる。

この方法のメリットは、多くのレイヤをカバーできることである。
一方で、デメリットは開発生産性にあり、以下のような懸念 / 制約がある。
※この記事は後続のテストコード方式を推奨としているため、このセクションは開発生産性上のデメリットにフォーカスして解説している。
テストコード方式は、より手軽で、サーバー起動方式の懸念 / 制約をカバーできる方法である。
これは、ユニットテストと同じ方式で、jestやvitest、xUnitなどのテストフレームワークを活用する。
ユニットテストとの違いは、アサーションの対象、ソースコードに対する呼び出し方など、書きっぷりだけである。

この方式のメリットは、開発生産性である。次のように、サーバー起動方式の懸念点を大きく軽減することができる。
一方で、テストコード方式のデメリットは、サーバー起動方式のメリットの裏返しで、APIテストとしての信頼性にある。
テストコード方式でカバーできるレイヤは以下のとおり、サーバー起動方式より少ない。ポイントだけ以下に述べる。

上記のようなデメリットがあるため、ミッションクリティカルなプロダクトであれば、より多くのレイヤをサーバー起動方式が望ましい場合もあるだろう。
しかし、逆に言えば、この方式はカバーしているレイヤこそ少ないが、最優先されるべきレイヤ2つはカバーできており、多くのプロダクトにとっては十分と考える。フレームワーク・コードの2レイヤさえカバーできていれば、アプリケーションの振る舞いの大部分はカバーでき、バグが発生するパターンが大きく絞り込まれるからである。
ランタイム環境レイヤがテストできていないことで起きうる問題は、「サーバー起動スクリプトのミス」等、ごく僅かなパターンだけだろう。また、ランタイム環境より下のレイヤは、そもそもアーキテクチャがサーバレスであったりした場合、サーバー起動方式を選択したとしてもカバーできない可能性もある。
むしろ、この方式の1番のリスクは、ヒューマンエラーにある。
テストフレームワークを使用しての記述となり、ユニットテストとの技術的方式の違いはないため、書き手次第でユニットテスト的なコードを書くこともできてしまう。
例えば、/users/{id}をテストするために、HTTPリクエストをエミュレートするのではなく、単にハンドラーを関数として呼び出した場合、このテストのAPIテストとしての信頼度は大きく低下する。
このハンドラーに/user/{id}という誤ったパスが割り当てられていたり、ミドルウェア(フィルター)等の機能で事前に意図しない処理が行われていても、検知できないからだ。
これは上から2つめのレイヤであるフレームワークレイヤを、テストに巻き込まない書き方ができてしまうということを意味する。

したがって、テストコード方式を選択する場合、チームとしてのAPIテストの書き方に関するガイドラインをしっかりと定めることが重要である。
よければTwitterもフォローお願いします!
@sumiren_t
バッジを受け取った著者にはZennから現金やAmazonギフトカードが還元されます。
