この記事は
この記事はこれは ただの集団 Advent Calendar 2020 の20日目の記事です。
業務でマイクロサービスのテストについて考える機会があったのですが、意外と日本語のまとまった資料がなかったので、Martin Fowler先生のTesting Strategies in a Microservice Architectureを参考にマイクロサービスにおけるテスト戦略をまとめました。
この記事では元のスライドにあるアプリケーションアーキテクチャ(アプリケーションレイヤー)についてはあまり言及していませんが、より具体的に理解したい場合は元のスライドを見ていただければと思います。
テスト戦略というよりかはテストの分類になっている感が強いですが・・語彙とそれが指し示す内容がチーム内・チーム間などで一致することは価値があると思います。
目次
- マイクロサービス・マイクロサービスアーキテクチャとは??
- Unit Test
- Integration Test
- Component Test
- Contract Test
- End-to-end Test
マイクロサービス・マイクロサービスアーキテクチャとは?
- こちらについてはAWS及びAzureの分かりやすい資料があるのでそちらを参照してください
Unit Test
- ユニットテストはアプリケーションで一番小さいテスト可能なソフトウェアで期待通り動作するかを判断するもの
- ユニットテストのサイズは厳密に定義されていないが、大体はクラスレベルなどの小さなサイズで行われる
- ユニットテストを書くにはモジュールを独立させる必要があるため、テスト戦略であると同時に特にTDDと組み合わせた場合には設計ツールにもなりうる
- Unit Testだけではシステムの振る舞いを保証することはできない
Unit Testの種類
- ユニットテストではコラボレーションするモジュールと分離するかどうかで重要視する点が異なってくる
- Martin Fowlerは2種類の視点のユニットテストを提示している
- 下記のスタイルは競合するようなものではなく異なる問題を解決するために用いられる
Sociable unit testing
- 状態の変化を観察することでモジュールの動作をテストすることに焦点を当てる
- 依存するモジュールにモック・スタブなどのテストダブルは(おそらく)使用せず、テストしたいモジュールをブラックボックスとして扱う
Solitary unit testing
- モジュールとその依存関係の間の相互作用に焦点を当てる
- 依存するモジュール自体の動きではなく、あくまで相互作用に着目するので依存先のモジュールはテストダブルを使用する
Integration Test
- インテグレーションテストはインターフェースの欠陥を検出するためにコンポーネント間の相互作用を検証する
- インテグレーションテストではモジュールを集めて、サブシステムとしての動作を検証する
- マイクロサービスアーキテクチャでは外部コンポーネントとの相互作用を検証するためによく使用される
- 外部コンポーネントとは他のサービス、データストア、キャッシュなど
- インテグレーションテストではモジュールのデグレや外部コンポーネントがデグレをおこした際などテストが失敗する理由が複数あり、これらの問題を軽減するために迅速なフォードバックが必要な特定のインテグレーションテストだけを書き、ユニットテストとコントラクトテストでコンポーネント境界のカバレッジを満たす方法もある
- ユニットテストとインテグレーションテストではクラスなどのユニットのロジック、インテグレートしたサブシステムのロジックについては意図した通り動いていることを確認できるが、ビジネス要件を満たしていることは確信を持てない
- ビジネス要件を満たしているかどうかは完全なエンドツーエンドテストによって確認できるが、外部の依存性から分離してテストすることで、より正確かつ少ないテスト時間でテストすることができる
Component Test
- そもそもここで言うコンポーネントとはマイクロサービスアーキテクチャにおけるサービスそのもののことを指している
- コンポーネントテストはソフトウェアを構成するマイクロサービスの中で一部のサービスをテストする
- サービス境界などネットワーク越しの通信が発生する箇所はスタブを使用したり、インメモリのデータソースを使用したりする。(この場合ネットワーク越しの通信は発生しないようにする)そうすることでテストの実行時間を短縮したり、動的に変更するリソースを少なくすることでビルド時の複雑性を減らすことができる
- 例えば外部サービスに接続するHTTP Clientはテスト時はスタブ用のHTTP Clientに差し替えたり、ネットワーク越しにデータベースにアクセスする箇所はインメモリデータストアに変更する
- ただしこのやり方の場合、テストモードとして起動する必要がある。
- 上記のやり方はネットワーク越しの通信を発生させないやり方だが、ビルドやインテグレーション、永続化が困難な場合、ネットワーク越しのデータストアや外部サービスにアクセスしてテストを行うことが適切な場合もある
- Martin Fowlerはデータストアはそのまま使うが、外部サービスについてはスタブサービスの使用を例として上げている。moco, stubby4j, mountebank, vcrようなツールを使用することで動的なデータやfixture、また記録したデータをレスポンスとして返すスタブを作れる(らしい)
- Scalaだと記録したデータをレスポンスとして返すスタイルのairframe-http-recorderというものがある
- ユニットテスト、インテグレーションテスト、コンポーネントテストを組み合わせることで網羅性を高めて、マイクロサービスのビジネスロジックが正しく実装されていることを確認できる
- だが、ここまでのテストでは外部依存との関係で期待される契約を満たしているか、マイクロサービスの集合体としてのソフトウェアがビジネスフローを提供するために正しく連携できているかを確認することはできない
Contract Test
- コントラクトテスト(契約テスト)はサービスの境界で利用者からみて、サービスが期待する動作をしているか(契約を満たしているか)を検証するテストのこと
- コントラクトテストはコンポーネントテストと異なり、サービスの動作を深くテストするものではないが、インプットとアウトプットに必要なものが含まれているか、またレイテンシやスループットが許容できる範囲かをテストする
- コントラクトテストを支援するツールとしてPact, Spring Cloud Contractなどのツールがある
End-to-end Test
- エンドツーエンドテストはシステムが外部の要件を満たしていることを確認するためにシステム全体を端から端(end to end)までテストする
- エンドツーエンドテストはシステム全体がビジネス目標を満たしていることを検証するので可能な限りシステムをブラックボックス化し、GUIやAPIのようなパブリックなインターフェースを介してテストする
- エンドツーエンドテストではサービス間のギャップをテストできるので、ファイアウォール、プロキシ、ロードバランサーなどのインフラが正しく設定されていることも保証できる
- エンドツーエンドテストはこれまでの他の戦略と比べ、動的な部分が多く含まれるので失敗する理由も多くなり、不安定なテスト、過剰なテスト実行時間、テストメンテナンスコストの増加を招く場合がある
- Martin Fowler先生はエンドツーエンドテストを書くこととメンテすることは非常に難しいと書いておりいくつかのガイドを提案している
-
- できるだけ少ないエンドツーエンドテストを書くこと
-
- ペルソナとユーザージャーニーにフォーカスすること
-
- 終わりを賢く選択すること
- 特定の外部サービスやGUIがテストを不安定にさせている場合、テスト境界を再定義して、他のテストなどでコンポーネントを検証しエンドツーエンドテストから除くなど
-
- 再現性のためにInfrastructure as codeに頼ること
-
- テストデータを独立させること
まとめ
- マイクロサービスアーキテクチャについて5つの戦略があることを見てきました。これまでの内容を元に、Martin Fowlerのスライド、特に最後のスライドをみることでどういった種類のテストがあり、モジュールやサービス通しがどのように協調して動くのかが改めて理解できるようになると思います。この記事が皆さんの理解の助けになれば幸いです。