コドモン Product Team Blog

株式会社コドモンの開発チームで運営しているブログです。エンジニアやPdMメンバーが、プロダクトや技術やチームについて発信します!

レガシーコードを理解して手放すためのAPIテスト導入

こちらの記事はコドモン Advent Calendar 2022の9日目の記事です。

qiita.com

こんばんは! コドモンプロダクト開発部QAエンジニアの水落です🍞

まずはじめに、読者の中にはコドモンをご利用いただいている方もいらっしゃるかと思います。2022年度に入って以来システムの稼働が安定しないことが時折あり、ご迷惑をおかけして誠に申し訳ございません。安定稼働に向けた取り組みを最重要課題として各種の対策を進めております。
 
その対策をより多くの優秀なエンジニアの方々と進めていきたいという意図もあり、今回はAPIテストを用いたシステム健全性のさらなる評価について書いていきます。

現在、コドモンにおいて課題になっているのが、最新のソースコードに対してテストにより動作・品質を担保するコストが高いことです。 テストは以前より継続して行っていますが、UI操作からのE2Eテストなど、ロジック詳細を追うには粒度の大きいテストでした。

サービスローンチから5年を経過し、人員も入れ替わっていく中で、結果としてソースコードの仕様を把握して改修するコストが高い状態になってしまいました。

そんな中、コドモンで使用しているミドルウェアを更新することとなり、更新後の動作担保のために改めてテストを整備する運びとなりました。 もがき、あがいた記録を赤裸々に書いていきます。

泥臭い内容ではありますが、お付き合いください!

対応のあらまし

対応前

主にAutifyによるE2Eテストと、本番模擬DBテスト、機能追加・修正時の手動テストを行っていました。

本番模擬DBテストは、本番模擬環境で処理をバッチ的に行い、修正前後で意図しない変更が加わらないか確かめるテストです。 「変わらないことを担保するテスト」としては明確なものですが、そのために環境を用意する必要があるなど、「いつでも動かせるテスト」という手軽さはありません。

手動テストは、実装部分とその周辺に対してテスト仕様書を作成して実施する、という最低限のテストであり、想定している範囲を中心としたテストでした。

ソフトウェアに関してはバタフライ・エフェクト的な不具合が起きうるため、システム全体を見渡しての動作担保としては、対応前は足りない部分があったと言えます。

また、ドキュメント類も十分でなく、仕様を知るには一部の有識者に尋ねるかソースコードを読み解くしか方法がありませんでした。 ソースコード内に適当なコメントもなく、またなかなか味わい深いソースコードで、求める答えを探すだけでもかなりの苦労がありました。

テスト対象

対象とする機能のすべてのAPIに対してテストを作成する、という方針で対応しました。ユーザーの操作は(適切に環境を整えた)API呼び出しに変換することができるためです。

またAPIは文字通り“Interface”であることから、Inputに対するOutputをテストすればよく、比較的テストしやすいものでもあります。 単体テストを入れられるほどソースコードが整理されていない状態でも、より外側からテストするAPIテストは導入できました。

テスト実装

ツール

GaugeAxiosを乗せた構成としました。 GaugeはMarkdownでStepを記載できるため、実行可能な仕様書*1を作成することができます。 仕様をドキュメントとしてソースと別に管理すると絶対に陳腐化するため、仕様書そのものが手軽にテスト実行可能というのは非常に嬉しいことです。

実作業

本対応ではミドルウェア更新が主目的であったことから、以下のような手順で作業を進めました。 延べ75本のAPIに対しspecファイルを作成し、1か月半を要しました。

  • specファイル(Gaugeの実行単位となるファイル)作成
    • 初期化Step(事前データのDB投入など)
    • ログインStep
    • リクエストStep
    • 結果比較Step(期待値JSONファイルを作成しておき、オブジェクト同士のdeepEqualで比較)
  • ミドルウェア更新前環境で期待値の作成
  • 同更新後環境で比較実施

悩んだポイント

カバレッジ(≒ 網羅性)

まずは広く浅いテストを目指しました。システム全体でのマクロなカバレッジを優先したためです。 条件網羅は「なるべくなぞる」程度とし、とにかく本数をこなすことに集中しました。

別途、テストを厚くしたいAPI(≒ 不具合発生時のインパクトが大きいAPIやビジネス的に重要なAPI)ではバリデーション・異常系も含めた各種パスのテスト作成も行いました。

事前データ範囲

必要なデータのみ都度insertする、というのは工数的に限界がある(そもそもどのデータがいつどのように必要なのかわかっていない)ため、「最低限ある程度のデータを用意したDB」のスナップショットを用意しておき、足りないデータのみinsertするようにしました。

当該DBにログイン情報を作成しておいたことで、各specファイルでのログインStepが1行の記載で済むなど、作業全体の高速化を図ることができました。

テストのメンテナンス人員

機能実装者がテスト作成にも携わるのが望ましいですが、複数人でのテスト実装では不必要なお見合いが発生してしまいがちです。

そのため、最初のテスト実装時点ではスピードを優先して、私自身がメンテナ(仮)としてテストのメンテナンスを一手に担いました。 その代わりに、他の開発メンバーにはプロダクトコードの修正に集中してもらいました。 いい意味での属人化、をとっています。

対応の結果

事前条件と仕様の整理

テストを実装する過程で足りないデータを補ったり、リクエストバリデーションのテストを作ったりする過程で、リクエストを受け付ける / 受け付けない条件がクリアになりました。

これが“仕様”の入口であり、APIを理解する突破口となる部分です。入口がクリアになっていれば、より細かい動作・仕様の理解が容易になります。

今まで改修のコスト/心理的ハードルが高かったAPIを、“Interface”として明確化し、積極的に扱う一助になります。

DX向上・対応高速化

テストの網羅性はまだまだ十分でないにせよ、「このテストを回しておけば最低限問題なく動作することが確認できる!」というのは開発者の精神安定につながります(と信じています)。

今までは軽微な修正も恐ろしくて負担が大きかったのですが、テストで動作担保ができソースいじり放題になれば、開発者は必要な実装にのみ集中でき、価値提供のスピードも向上します。

また今回のようなミドルウェアの更新も、今回ほど対応に時間をかけずに済みます。脆弱性を排除するためにも、ミドルウェアの変更にすぐ追随できることは重要です。

リファクタリング・リプレイスに向けた動き

一部APIのテストを厚く作成した結果、リファクタリングに十分耐えうるテストが作成できることがわかりました。 APIテストで内部構造まで踏み込んだテストを作成するのはアンチパターンかもしれませんが、「リファクタリングをするためのリファクタリングが必要」な状態のソースコードに対して分岐を概ね網羅したテストを作成できるのは利点です。

今までは「リファクタしたいけど……」というところで手も頭も止まってしまうことが多かったのですが、リファクタリングが現実的に可能になったのは大きな一歩になりそうです。 また「もうリファクタできるから」と頭の中の心配事から外せるのも、精神的には大きいです。

課題と展望

作業標準化

本対応の多くを私一人で行ってきました。対応スピード向上とナレッジ共有をトレードオフした形です。

テストはソースコードの修正とペアで行われるべきものであり、常に実装者による正しいメンテナンスが必要です。 テストの実装自体は難しいものではないにせよ、細かいノウハウはどうしても存在するため、誰でもテストを追加・変更できる状態に持っていく必要があります。

とは言え、テストスイートとしての整合性を保つため、私は引き続きメンテナ(仮)としてレビュワーの一人でいるつもりでおります。

網羅性

まず1パス、を目指してテストを作成したため、あらゆる数多全ての分岐を完全に網羅しているとまでは言えません。 しかし1パスを通せたことで、最低限必要なパラメータや事前データがある程度わかっています。 対応着手前よりもAPI自体の解像度が上がっているため、網羅性まで考慮したテストの実装ハードルはグンと下がっています。

また、APIテストで仕様(と内部設計)が明らかになってきているため、リファクタリング実施と合わせての単体テスト導入も可能になる見込みです。 分岐網羅は単体テスト、よりマクロなリクエストに対するレスポンスの確認はAPIテスト、といったようなスコープと責務に合わせたテスト作成にも移行できそうです。

レスポンス確認

現状、作成したテストはリグレッションテストメインのため、レスポンス内容のテストは「期待値ファイルとの丸ごと比較」です。 より“仕様”を明示するためにはレスポンスのフィールドごとのアサーションを作成することが理想です。

そのようにテストを整備できれば、OpenAPI(Swagger)のようなフォーマットには沿っていないにせよ、内部で使用するAPI仕様書としては十分実用に耐えうるものになると考えています。

おわりに

私個人としてはテストは仕様の具体化である認識であり、切っても切れない関係であると考えています。 よくあるV字モデルでも、テストと仕様は同じものを違う視点から見ています。ソースコード・仕様を理解するためのテスト作成、というのもアプローチとしては間違っていないと考えています。

ソースコード・テスト・仕様はどれも可塑性のあるもので、常にその時に合わせて形を変えていきます。 その変化に追随できるようにするためには、私一人の踏ん張りだけではどうにもできず、他の開発メンバーにも協力をお願いしています。 いつも助けられており、心の中で「いつもありがとうなあ〜〜〜ラブ〜〜〜」と叫んでいます。

今回作成したテストはあくまで“つなぎ”としてのテストで、リファクタリングが進めば単体テストに委譲するなどして、いつかは今回作成したテストを手放す方向で動いていきたいと思っています。 手放すときに「そんなこともあったなあ」と懐かしく思える状態になっているのが理想です。

「触れたくないソースコード」というのは往々にして存在します。 しかし、ただフタをするだけでは発酵するか腐敗するかわかりません。誰にでもある黒歴史と同じですね。

風通しをよくして、軽やかに手放してみませんか?


コドモンの開発チームのTwitterを始めました! アドベントカレンダーの新着記事も毎日ツイートしていくので、ぜひフォローしてください😊

twitter.com