freeeの開発情報ポータルサイト

テストアーキテクチャの実践

こんにちは。決済プロダクトでQAエンジニアをしているrenです。freee QA Advent Calendar2024の23日目です。
今回は、決済プロダクト開発にテストアーキテクチャを設計した事例について紹介します。

テストアーキテクチャとは

テストアーキテクチャとは「テストをどう実現するか」の基本的な概念や特徴を、テストの要素、関係性、設計原則・方針で表現したものです。*1
テストの全体像や構造、設計指針を明らかにすることで、テスト活動の継続的な改善が可能になります。

参考資料:

テストアーキテクチャによって解決したい課題

決済プロダクトの開発チームでは以前からテストに関する改善活動を積極的に行っており、一つ一つの改善による成果も出ていました。*2
その一方で、より効果的で効率的なテスト活動を実現するためには、テスト活動全体の整合性確保や、改善活動から得られた知見の解像度を上げる必要性が見えてきていました。

具体的には、以下のような課題を解決するためにテストアーキテクチャを設計しようと決めました。

  • テストレベルごとの責務を定義し、プロダクトに対する「テストしたいこと」が全体的にどのようにテストされるべきなのかを理解できるようにする
  • テストの重複をなるべく減らし、テスト活動の効果と効率性を最大化する
  • テスト要求をテスト活動にマッピングすることで、今の自分たちがテスト要求をどのように満たしているのか、そしてどのような改善点があるのかを理解できるようにする

テストアーキテクチャ設計のステップ

今回取り組んだプロダクトの状況は、以下の通りです。

  • テスト対象ソフトウェアはモジュラモノリス*3を採用しており、各モジュールではクリーンアーキテクチャ*4が採用されている
  • FrontendはReact (TypeScript), BackendはGoで書かれており、go testで書かれたUnit TestやIntegration Testも存在している*5
  • QAエンジニアがスクラムチームに入り込んで常に並走するアジャイルQA*6という体制を取っており、QAエンジニアと開発エンジニアのコミュニケーションは日々活発に行われている

このような状況の中、「自動テスト実装の実例を積み上げて然るべきタイミングで抽象化してテストアーキテクチャを設計する」というアプローチで、およそ半年間かけてテストアーキテクチャを設計しました。

1. この振る舞いを担保するためには、どのようなテストが必要だろうか?という問いを立てる

まず初めに、リグレッションテストケースなど、既に自分たちが内容を理解している機能のテストを題材に、「この振る舞いを担保するためには、どのようなテストが必要だろうか?」という問いを立て、適切なテストケースを特定し、実装する取り組みを行いました。
最初にこのようなことに取り組んだのは、自分たちがテストしたい目的は何なのか、テストしたい対象は何なのかを理解した上で、「このプロダクト開発において、どのような自動テストを実装するべきなのか」を考える…という考え方を身につけたかったためです。
テストを導くためのテストアーキテクチャの組み立て方/cetam - Speaker Deck の中でもテストアーキテクトのアンチパターンとして「象牙の塔のアーキテクト」が挙げられており、このアンチパターンを避けるために、まずはテストアーキテクチャという概念は抜きにして、自動テストの実例を積み上げるアプローチを選択しました。

実際、このテストの特定/実装の取り組みを繰り返すことにより、自分たちが開発しているプロダクトの設計とテストをどう関連づけられるかの理解を深めることができました。また、QAエンジニアと開発エンジニアのコミュニケーションもより増えたため、次のステップに挑戦する土台形成にも役立ったと感じています。

QAエンジニアと開発エンジニアのコラボレーションでは、QAエンジニアがテストケースの分析/設計を行い、開発エンジニアがその実装方法を提案するという協業が効果的でした。例えば、決済処理の正常系フローについて、QAエンジニアが「決済金額算出のロジックの正確性を担保したい」という要望を出し、開発エンジニアが「これはドメインロジックのUnit Testで実装できる」という具体的な提案を行うといった形です。

2人のイラストが話し合っている場面。左の人物が「決済金額を算出するロジックの正確性てどう担保できますかね。こういうパターンを見たいのですが...」と話しかけ、右の人物が「そこなら、ドメインロジックのUnit Testで実装できると思います。既に幾つかテストを実装しているので、テストパターンのレビューをお願いしたいです!」と答えているシーン。

2. 抽象化して考えるための概念を学ぶ

上記の取り組みにより自動テストの実例が増えてきたタイミングで、テストサイズやテストレベル設計、テストアーキテクチャなどの概念についての勉強会をチームで行いました。

まず、freeeで整備されている標準テストプロセスをテーマに、テスト活動の流れについて開発チーム全員で改めておさらいしました。
参考資料: speakerdeck.com

次に、基本的な用語を押さえるために、JSTQBのFLシラバスやymty-sanのnoteを用いて勉強会を行いました。
参考資料:
- https://jstqb.jp/dl/JSTQB-SyllabusFoundation_VersionV40.J02.pdf
- フェーズ、レベル、タイプ、技法|Tsuyoshi Yumoto

そして、用語の理解をした上で、テストアーキテクチャに関する概念を理解するための勉強会を行いました。 参考資料:
- LINE Developer Meetup in Tokyo #39 Presentation (modified) | PPT
- モダンなテストレベル設計(ユニットテスト~システムテスト等をどう設計するか)の原則 - 千里霧中
- テストを導くためのテストアーキテクチャの組み立て方/cetam - Speaker Deck

3. テストアーキテクチャを描き、議論する

上述の取り組みを通してテストアーキテクチャを設計する土台が整った後、叩き台となるドキュメントや図を作成してチームで議論を行いました。

チームでの議論は、テストレベルの責務の明確化やMockの使用方針などについて展開されました。 例えばMockの使用方針については、これまでは主にUsecase層のテストにおいて細かくMockを作成していたのですが、今後はUsecaseを実行するrpcメソッドやbatch処理の単位でintegration testを書き、そのテストではrepositoryやclientの実物を使うようにしようという結論が出ました。
この決定の背景には、Mockの過度な使用がテストの実装やメンテナンスを複雑にしているという、開発メンバー間で共有されていた課題感がありました。

バックエンドテストの設計図を示す画像。左側にusecaseのテスト構成と、右側にIntegration Testの構成を比較して表示しています。左側では、usecaseからrepository mockとHTTP client mockを経由してDBと依存サービスに接続する構成を示し、右側ではusecaseから直接repositoryとHTTP clientを通じてDBと依存サービスに接続する構成を示しています。画像の右下には「技術の日2024」のロゴが配置されています。
https://speakerdeck.com/ropqa/implementing-tests-as-a-team?slide=42

テストレベルの責務についても議論を行いましたが、既に自動テストの実例が十分に存在する状態での議論だったため、言葉の定義や解釈を確認しあう程度でした。
抽象的な事柄について空中戦になることを避けて議論できたのは、議論の時間を無駄にしないという点と現実に根差した納得感を醸成するという点で良かったと感じています。

今回定義したテストアーキテクチャ(テストレベルの責務定義やテストコンテナ図)は、記事の最後に記載しています。

テストアーキテクチャ設計のインパクト

実装改善に関する気づき

議論を通じて実装改善の機会も見出されました。具体的には、Usecase層に存在しているバリデーションロジックは、domain層やadapter層に適切に分散させるべきという気づきが得られ、今後の開発指針として採用されることとなりました。

テストの構造とソフトウェアの構造に関する気づき

テストアーキテクチャの設計を通じて最も重要な発見は、「テストレベルの責務は、ソフトウェアアーキテクチャに概ね沿う形で定義できる」ということでした。
テスト分析上の分類がソフトウェア設計に合致するケースが多くあり、機能実現のために各コンポーネントにどのような責務を負わせるのか?という設計上の意思決定を前提に、テストアーキテクチャの構築を進めることができるのではないか、という気づきを得られました。
これは、今後異なる種類のプロダクトに対してテストアーキテクチャ設計を行う際に活かせる仮説を得られた、という点で重要な発見だったと考えています。

自分たちのテスト活動の課題に関する気づき

今回テストの構造を整理してみて、middlewareのテストや非機能テストをまだ十分行えていないかもしれないという課題も見えてきました。
今のテスト実施状況や何となく思っていた理想を言語化/図示化することで、自分たちがどうテストしているのかがわかったり、どの辺りのテストに課題があるのか発見できたりするのは、大きなメリットだと感じました。

チーム開発の支援を通じた工数上のインパクト

特にリグレッションテストにおいて、手動で実行していた多くのテストがUnit TestやIntegration Testに置き換えられ、実行時間短縮に繋がりました。具体的には、それまでリリースのたびに20時間程度かかっていたリグレッションテストを、最大でも2時間程度で終わらせられるようになりました。

また新機能開発においても、テストアーキテクチャが明確になったことで、実装するべきテストの判断が容易になり、開発効率の向上を実感することができました。

テストアーキテクチャがもたらす価値

私たちがテストアーキテクチャを設計して得られた気づきやインパクトは上述の通りですが、改めて「テストアーキテクチャがもたらす価値」をまとめると、以下のように整理できると思います。

  1. テストの全体像の可視化

    • 現状のテスト実装の適切性評価
    • 課題の早期発見(例:middleware testingや非機能テストの不足など)
  2. 開発速度と品質の向上

    • テストレベルの責務最適化によるシフトレフト
    • テスト実行時間の短縮
    • テストの保守性向上
    • 望ましい設計指針の発見
  3. チーム間の認識統一

    • テストの責務に関する共通理解
    • テスト設計・実装の指針の明確化

今後の展望

今回は決済プロダクト開発での事例を紹介しました。
今後は、異なる種類のプロダクトへの適用をしていきたいと考えています。多くのプロダクトへの適用によって、テストアーキテクチャの構築方法やその運用方法をより柔軟に適用可能なものに進化させたいです。

明日は、QAマネージャーのuemu-san が「Webサービスの歩き方 - 境界値分析-1.0」について記事を書いてくれます。お楽しみに〜!

よい品質を〜


[appendix]今回設計したテストアーキテクチャの一部

テストレベルごとの責務

私たちが開発している決済プロダクトは主にバックエンドがGo言語、フロントエンドがTypeScript (React)で書かれています。 アーキテクチャはプロダクトによって多少異なる部分がありますが、マイクロサービスやモジュラモノリスによるモジュールごとにクリーンアーキテクチャが採用されています。

このようなアーキテクチャを前提に、テストレベルごとの責務を定義しました。

テストレベル 責務 要点
Unit Test (backend) / 単体テスト ドメインロジック / ビジネスロジックを担保する
- domain/entity層
- usecase層に存在するバリデーション(※)
- adapter層にあるoutgoingなclient
- 入力値と出力値の網羅度合いを重視するため、「Table Driven Test」にする
- 内部実装が変わっても処理のI/Oが変わらないことを担保するために、PVはブラックボックステスト的に導出する
- (※)domain, adapter層に割り振れないバリデーションは基本的に無いはずなので、usecase層のバリデーションが存在する場合は他の層への移植を検討する
Unit Test (frontend) / 単体テスト 純粋関数やカスタムフックのロジックを担保する -
Integration Test / 結合テスト コンポーネント間の相互処理を含む、ひとまとまりの機能性 / フィーチャーを担保する
- private / internal API
- batchやworkerを含む一連の処理
- 実装/運用上のコストがUnit Testよりも高いため、gRPC, batch, workerなどのentrypointに対して「最長のハッピーパス」を少なくとも1つ書く
System Test / システムテスト プロダクトのエンドツーエンドの振る舞いを担保する
また、性能や信頼性などの非機能品質特性も担保する
- 実行時間が最も長くflakyになりやすいため、E2Eテストの実装対象は原則クリティカルパスに絞る
(クリティカルパスとは、「そのユースケースが動かなかったら重大な障害になるもの」である)

テストコンテナ図

テストレベル間の関係を視覚的にも理解するため、テストコンテナ図*7も描きました。
まだざっくりとしか記述できていないので、今後の運用を通じてより役立つものに進化させていきたいです。

テスト階層の全体像を示す図で、5つの主要なテストカテゴリーが表示されています:  UnitTest(左上) 機能テスト:ドメインロジック/ビジネスロジック 外部サービスとのreq/res IntegrationTest(中央上) 機能テスト:コンポーネント間の結合 APITest 機能テスト:無効パーティション セキュリティテスト:認証認可 SystemTest(右上) 機能テスト:エンドツーエンドの振る舞い その他、GUIや他サービスがあることで可能になるテスト 性能テスト 使用性テスト 信頼性テスト セキュリティテスト RegressionTest(中央、緑色) 機能テスト 重複度major以上の事象の発見を見込むテスト 外部サービスとの結合テスト 本番テスト(下部、青色) 機能テスト:試験環境では見切れない、実際の動作性テスト 各テストカテゴリーは明確に区分けされ、それぞれの目的と対象範囲が示されています。
テストの関連を示すテストコンテナ図

*1:引用元:テストを導くためのテストアーキテクチャの組み立て方/cetam - Speaker Deck

*2:freee技術の日2024で発表した内容もぜひご参照ください。チームでテストを実装していく / Implementing Tests as a Team - Speaker Deck

*3:すべてのコードが単一のアプリケーションを動かすシステムであり、異なるドメイン間に厳密に強制された境界があるシステムのこと。モノリスとマイクロサービスの両方のメリットを得ることができる。参照:[翻訳] Shopifyにおけるモジュラモノリスへの移行 #Ruby - Qiita

*4:機能を実現しているコアな部分をフレームワークやDBなどに依存しない状態(関心事の分離)にすることで、他が変わってもコアな部分への影響をなくし、変更や拡張に強くすることができるアーキテクチャ。参照:やさしいクリーンアーキテクチャ

*5:自動テストについて検討する上で、参照できる自動テストの実装が存在している状態

*6:【連載 第8回】QAがfreeeカードUnlimitedのスクラムチームメンバーとして取り組んでること - freee Developers Hub

*7:大規模なテスト設計において、全体像を把握しやすくするために描く図。参照:https://www.jasst.jp/symposium/jasst16tohoku/pdf/S1.pdf