こんにちは、株式会社LegalOn TechnologiesのLegalForceキャビネ開発部でQAリードを務めている島根(@shimashima35)と申します。
QAというとマニュアルテストが中心かと思われるかもしれません。確かにマニュアルテストはQAの業務の一部ではありますが、「質とスピードの両立」つまりプロダクト品質の高さとリリーススピードの両立を目指すため自動テストの導入もおこなっています。
今回はLegalForceキャビネのバックエンドに対するAPIテストを実装した話をご紹介します。
APIテストとは
まず最初にAPIテストについて説明します。
APIテストの概要
APIテストとはWeb APIの外形的な仕様を満たしているかを HTTP(S)を用いてテストすることです。バックエンドのコントローラーに対するユニットテストとは以下の点が異なります。
- HTTP(S)を経由する
- データストアまで含めて結合している
- コードカバレッジを意識しない
テストピラミッドにおける Integration Test
に相当するものです。
APIテストのメリット・デメリット
APIテストの概要を説明しましたが、次にAPIテストとユニットテスト・E2Eテストとの比較をしていきます。
ユニットテストとの比較
APIテストにはユニットテストと比べて以下のようなメリットがあります。
- Web APIそのものについてのテストが行える
- 結合した状態での振る舞いの正しさを確認できる
一方で以下のようなデメリットがあります。
- データベースなど外部の状態に依存するので壊れやすい
- 実行速度が遅い
- 記述量が一般に増える
ブラウザテストとの比較
APIテストはブラウザテストと比べて以下のようなメリットがあります。
- 実行速度が速い
- 壊れにくい
一方で以下のようなデメリットがあります。
- 表示に関する問題を検出できない
- ブラウザ依存の問題を検出できない
APIテスト導入に至る経緯
前提および背景
モチベーションの前に、APIテストを導入する前のLegalForceキャビネのテスト周りの状況について説明します。
LegalForceキャビネでの自動テストは以下のような状況でした。
- ユニットテストはフロントエンド・バックエンドともここ1年くらいの新規部分のカバレッジは高い一方、古い部分のカバレッジは低い。
- ブラウザテストはmablを使いリスクベースで優先順位を決めて書いているが、全体としての機能カバレッジはまだ低い。
全体的には自動テストを導入しているもののまだマニュアルテストが中心となっていました。
APIテスト導入のきっかけ
APIテストは以前から導入したいと漠然と考えていましたが、LegalForceキャビネのアーキテクチャ変更により具体化しました。性能・キャパシティ向上を目的としてデータストアをそれまで使っていたFirestoreからMySQLへの移行が決まったためです。
LegalForceキャビネでは、フロントエンドがバックエンドのWeb APIを呼び出しその結果をもとにレンダリングする、いわゆるSPA(Single Page Application)の構成をとっています。そのため、API仕様は変わらないものの、バックエンドのかなりの部分を書き換える必要が出てきます。書き換えるということは必然的にリグレッションテストを行う必要も出てきます。
今あるユニットテストとブラウザテストだけではカバレッジの関係でリグレッションテストの範囲全てはカバーしきれません。また、ブラウザテストのシナリオを拡充するという手段も検討しましたが、以下の点で好ましくありません。
- 一般にブラウザテストは他の自動テストと比較してメンテナンスコストが高いため、むやみにケースを増やしたくない。
- API内部のバリデーションのテストを行う際、ブラウザテストではUIレベルでバリデーションが行われるため、目的のテストができないことがある。
上記の前提を踏まえた結論として、APIテストを実装しAPIレベルでバックエンドの実装を担保する仕組みを作ることにしました。理由は、バックエンドのデータストア変更のみでAPI仕様は変わらないためリグレッションテストとして機能すること、作ったAPIテストは資産として使い続けることができるからです。
技術選定
次に行ったのは、具体的にどの言語でどのツールを用いるかを選定することです。候補としては以下のようなものがありました。
これらのツールについて以下の観点で絞り込んでいきました。
- テスト記述言語
- 言語処理系
- シンプルさ
結論からいうと、上記で挙げたフレームワークは利用せず、既に利用していたJestのみを用いることになりました。以下に決定までの経緯を説明します。
LegalForceキャビネはフロントエンド・バックエンド共にTypeScriptで実装され、バックエンドはNode.js上で実行されます。APIテストの導入はQA・SET(Software Engineer in Test)が主導するもののテストの追加・保守はバックエンドエンジニアが行うことになるので、バックエンドと言語・環境を揃えることが望ましいです。
また、導入にあたりフレームワーク固有の知識を過度に要求するものも学習障壁になるため可能な限り避けたいものです。
KarateはGherkinによるDSLでテストをかけるので言語非依存という点ではよいですが、実行環境としてJavaVMを要求します。既存インフラでJavaVMはないので、このためにJavaVMを用意するのは望ましくありません。
REST Assured はテスト記述・実行環境ともにJava/JavaVMなので却下。
PlaywrightとCypressはともにTypeScriptで記述しNode.js上で実行できるため今の環境との親和性が高くよいものでした。
その後社内のTypeScript有識者と話をしたところ、
- APIテストは行うことが非常にシンプル
- JSONはJavaScript/TypeScript組み込みでパースできる
- 余計な依存は増やしたくない
との意見があったため、フレームワークの利用を辞めることにしました。
APIテストの構成
APIテストの基本的な構成は以下のようになっています。
GitHubのPull Request毎にGitHub Actionsで起動し、TypeScriptで実装されたAPIテストが実行されます。APIテストはFirebase Authenticationによる認証をおこなった後、Cloud Functionsで実装されたバックエンドAPIをHTTP(S)で叩き、その戻り値を検証します。
APIテスト導入時の課題
導入以前はAPIテストでは、HTTPリクエストを投げて返ってきたJSONを確認するだけと考えていましたが、実際に導入しようとするといくつもの課題が見えてきました。
ここではそれらの課題とその解決について解説します。
課題1:リクエスト頻度の制限
APIテスト構成図の通り、LegalForceキャビネでは認証についてはFirebase Authenticationを利用しています。APIテストを実行する際にも、その認証を行う必要があります。
最初の実装ではリクエストごとにFirebase Authenticationでの認証を行なっていました。
しばらくはこれで問題ありませんでしたが、複数人でテストを実行した際に呼び出し回数の制限によりエラーになるという問題が発生しました。公式ドキュメントに記載はないですがどうやら単一IPからのRateLimitがあることがわかりました。
対策として、以下のような実装変更を行いました。
- jestのGlobalSetupでFirebase Authenticationの呼び出しを行う。
- 上記で取得した認証キーをすべてのテストで利用する。
このようにすることでテストスイート全体で認証の実行は1回に抑えられ問題は発生しなくなりました。
課題2:大人数でのテスト・実装方針の統一が難しい
APIテストの基盤部分は私ともう一人のSETで実装しましたが、APIの変更時のテスト修正があるため、個別のテストについてはバックエンドエンジニアにも書いてもらうつもりでした。
少人数の場合、テスト・実装方針の共有は口頭レベルでもある程度機能しますが、バックエンドエンジニアまで含めた数になるとコミュニケーションパスが増えるため難しくなります。 そのため、以下を行うことでその解決を図りました。
APIテスト実装ガイドではAPIテストをどのように書くか、そしてどの範囲まで担保するかを明文化し、もくもく会ではバックエンドエンジニアを集め、そのガイドをもとにAPIテストを実際に書いてもらうというものです。
これにより、APIテストの記述方針を揃えることができ、また実際にAPIテストを書くことでバックエンドエンジニア自身で実装・保守が行えるようになりました。
それだけではなく、大人数でテストを実行したことで課題1のような問題の発見にも繋げることができました。
課題3:テストデータの環境依存
APIテストの構成
で書いたように、APIテストはMockではないDB(Firestore)まで結合した状態で実行されます。このため、必然的にAPIテストはデータ依存を引き起こすことになります。このことで問題になるのは以下の3点です。
1の問題に対しては愚直にテストデータを作り投入していくしかありません。具体的にはWeb画面から目的のデータを登録・変更していく作業を、テストで必要な分だけ実施しました。
2の問題は、LegalForceキャビネのデータが顧客ごとに独立していることを利用し、APIテスト用のテナントを作成することで解決しました。
3つ目の問題ですが、これは以下のようなものです。
APIテストはリグレッションテストの一部としてビルド毎にGitHub Actionsからステージング環境で実行されます。しかし、このタイミング以外にも開発中のAPIに対して開発者毎の個別環境で動かしたいという要望もありました。
この問題に対してはステージング環境のAPIテスト用のデータをコピーするスクリプトを作成し、どの環境でも実行できるようにしました。
課題4:CIでの安定化
UnitTestではあまり起きないことですが、弊社で実装したAPIテストではタイミングの問題などによるFlaky Test(実行結果が不安定なテスト)の問題が発生しました。ローカルで実行している分にはそのまま再実行で良いのですが、上で書いたようにGitHub Actionsによる自動実行でしかもこれが通らないとビルド環境と見做さない運用をしているので致命的です。
これに対しては、APIテストの実行基盤であるJestの --onlyFailures
オプションを利用し、以下のように失敗したテストのみ再実行を行うようにしました。
yarn jest tests_api/ || yarn jest tests_api/ --onlyFailures
rerunは一回のみですが、これにより以前より安定性が増し、また再実行による時間増加も --onlyFailures
を使うことで最小限にしました。
課題5:破壊的なAPIのテスト
課題3でもあげたAPIテストでのデータの扱いですが、事前状態だけでなくテストの再現性を担保するためにはデータを更新した場合そのままにしておくことはできません。
この問題に対しては、愚直ではありますが更新処理を行うテストの事後処理に元に戻すAPI呼び出しを追加することで対応しました。これにより、APIテストを何度実行しても常に同じ状態で始まるため、データ不整合で失敗することはありません。
ただ、この方法だとテストの記述量が増えてしまうのと複雑性が上がるため、テスト用のデータを予め外部に保存しておき、テスト実行後に保存したデータを投入し初期状態に戻すということも計画しています。
APIテストを導入した結果
FirestoreからMySQLへの移行は今まさに佳境を迎えています。APIテストを作り込んでおいたため、手動でテストしなければいけない部分は大幅に減り、テスト工数の圧縮は現時点では成功しています。
また、APIテストはデータ移行だけでなく普段の開発でもバックエンドのリグレッションテストとして活用されています。定量的なデータはとっていませんが、API仕様が変わらないバックエンドの修正についてはほとんどAPIテストのみでリリースをおこなっている状態になっています。
おわりに
LegalForceキャビネではAPIテストの導入を進めてきました。APIテストの導入を進める中、多くの課題がありました。しかし、同僚でSETの引持(@rmochioo)の協力もあり、MySQL移行時のテスト工数を圧縮することができました。
弊社でのAPIテスト導入については、2022年12月5日に開催されたソフトウェアテスト自動化カンファレンス2022にて引持による「APIテストにおけるテスト駆動テスト実装のすゝめ」というタイトルでこのBlogとは別の観点で発表されています。この記事を執筆している時点ではまだ資料・動画は公開されていませんが、いずれ公開されると思いますのでそちらも参照していただけると幸いです。
メンバー募集中!
弊社では一緒に自動テストや「質とスピードの両立」のための仕組みづくりを行うSoftware Engineer in Testを募集しています。まずは、カジュアルにお話しましょう!
TECH-702- SET / Software Engineer in Test - 株式会社LegalOn Technologies