マイクロサービスアーキテクチャにおけるテスト戦略

:::note warn
 記事の内容はあくまで個人の考えに基づくものであり、組織を代表するものではありません。
:::

## 挨拶と背景

こんにちは、[シルバーエッグ・テクノロジー株式会社](https://qiita.com/organizations/silveregg)のtepと申します。
昨年8月より、プロダクト開発エンジニアとしてWebサービスの開発を行なっています。

弊社ではマイクロサービスアーキテクチャでサービスを運用しています。
ここまでの経験を経て、開発やデプロイについてある程度勘所が掴めてきたと思う一方で、
テストについては未だに手探り感が否めず、アーキテクチャに合わせた手法を検討する余地がありそうだと感じています。
より良い手法を見つけるべく、マイクロサービスアーキテクチャにおけるテスト戦略のベストプラクティスを整理したいと思います。

## アジェンダ
- なぜマイクロサービスアーキテクチャにテスト戦略が必要なのか
- テストの種類
- まとめ

## なぜマイクロサービスアーキテクチャに特別なテスト戦略が必要なのか

そもそもマイクロサービスアーキテクチャだからと言って、なぜ特別なテスト戦略が必要なのでしょうか。
[Best testing strategies in a Microservice architecture](https://www.cigniti.com/blog/microservices-architecture-testing-strategies/)では以下のように説明されています。
>Why do you need a special strategy to test microservices? 
>You need a different strategy to test microservices as they follow a different architecture and have a lot of integrations with other microservices within one’s organisation as well as from the outside world (3rd party integrations). Additionally, these require a high amount of collaboration among different teams/squads developing individual microservices. Moreover, they are single purpose services and are deployed independently & regularly. 

つまり、
- マイクロサービスはそれぞれ異なるアーキテクチャに従い、 組織内および外部からの他のマイクロサービスと統合を行っている
- それぞれのマイクロサービスを開発する異なるチームやSquad間の大量のコラボレーションが必要
- それぞれのマイクロサービスはそれぞれ単一の目的を持ち、独立して定期的にデプロイされる

という特徴があり、サービスやチームが分散化し独立して活動することで、モノシリックアーキテクチャにはなかった境界が生まれる。
そのため、その境界間のやり取りを考慮した戦略が必要ということかと理解しました。


## テストの種類
テストの種類は大きく以下の5つを用いることが多いようです。

1. Unit Testing
2. Integration Testing 
3. Component Testing 
4. Contract Testing
5. End-to-end Testing


「microservices testing strategy」というキーワードでヒットした10件程度の記事を調査する中で、上記5つを紹介しているものが最も多かったです。恐らく現時点での最も主流の構成のようなので、この構成を詳しく確認していきたいと思います。

### Unit Testing
- 単体テストは、アプリケーション内のテスト可能なソフトウェアの最小部分を実行して、期待どおりに動作するかどうかを判断する
- 特定のマイクロサービスの内部モジュールのテストを記述する
- テストダブル、スタブ、 およびモックを組み込んで、依存関係を置き換える

### Integration Testing

- 統合テストは、モジュール間およびモジュール間の依存関係間の通信経路と相互作用を検証する
- モジュール間の統合テストの例は、 リポジトリとサービスレイヤー、 サービスレイヤーとリソースなどの間の通信を検証するテスト
- 外部依存の統合テストの例には、データストア、キャッシュ、 およびその他のマイクロサービス間のテストが含まれる  
- 単体テストフレームワークを使用して統合テストを拡張するのが容易なため、自動統合テストの作成作業を開発者に委任することが推奨されている
- 開発者が既存の関連する内部モジュールの実際のインスタンス（例：APIリソース、サービスレイヤー、DB、メッセージキューなど）を必要とする
- 実際の依存関係をすべて配備した統合環境を提供する準備ができている場合に自動統合テストの手法を選択することができる
- 完全な機能テストは、他のテストタイプ（コンポーネントテスト）でカバーされるため、網羅的なリストを自動化しないようにする
- 小さなテストスイートサイズで、統合レイヤーの間のヘルスチェックのように動作する必要がある

### Component Testing 

- ユニットテストとインテグレーションテストでは、マイクロサービスを構成する内部モジュールの品質に対する信頼は得られるが、マイクロサービスが全体として連携してビジネス要件を満たすことを保証できない
- 統合されたマイクロサービスを外部の依存関係や他の連携するマイクロサービスから分離し、エンドツーエンドでテストする必要がある
- コンポーネントテストでは、依存関係をテストダブルサービスやモックサービスに置き換えることで、指定されたマイクロサービスのエンドツーエンドの機能を分離して検証する
- サービスの分離は、外部のコラボレータをテストダブルズに置き換え、内部のAPIエンドポイントを使用してサービスを調査したり設定したりすることで実現する
- コンポーネントテストはマイクロサービスAを分離してテストし、次にBをテストし、次にCをテストといったように、一度に1つのマイクロサービスに焦点を合わせて個別に行う
- コンポーネントごとに個別にテストすることで、次のようなの利点が得られる
    - テスト範囲を単一のコンポーネントに限定することで、そのコンポーネントが内包する動作の受け入れテストを徹底的に行うことができ、かつ、テストの実行速度も維持できる
    - テストダブルやモックを用いて、そのコンポーネントを他のコンポーネントから分離することで、他のコンポーネントが持つ複雑な挙動を回避することができる。また、そのコンポーネントに対して制御されたテスト環境を提供し、適用可能なエラーケースを繰り返し発生させることができるようになる

### Contract Testing 

- ユニットテスト、インテグレーションテスト、コンポーネントテストでは、マイクロサービスを構成するモジュールの高いカバレッジを達成することができる
- しかし、テストが一緒に動作するすべてのマイクロサービスをカバーしない限り、完全なソリューションの品質は保証されない
- 外部依存のコントラクトテストとシステム全体のエンドツーエンドテストは、これを提供するのに役立つ。
- コントラクトテストは、外部サービスの境界で、サービスを利用することによって期待されるコントラクトを満たしているかどうかを検証するテスト
- コントラクトテストは、2つのステップで行われる
    - 第一ステップ：コンシューマはプロバイダに対してコントラクトを発行する
        - このコントラクトは、典型的なAPIスキーマ（jsonファイルで可）のようなもので、考えられるすべてのリクエスト、レスポンスデータ、フォーマットが含まれている
        - ヘッダー、ボディ、ステータスコード、Uri、パス、動詞などが含まれる
        - 第一ステップの終了時に、コントラクトはプロバイダーに直接、または中央の場所に発行される
    - 第二ステップ：プロバイダが消費者から渡されたコントラクトにアクセスし、最新のコードと照らし合わせて検証を行う
        - もしエラーが見つかったら、それはプロバイダが自分のAPIスキーマに加えた、何らかの破壊的な変更が原因である可能性がある。
        - こうすることで、プロバイダチームはすべてのコンシューマ（組織内の異なるチーム、または異なるクライアントや顧客）にスキーマの変更に関する最新情報を提供することができる
        - これは、コンシューマが前もって変更管理を計画するのに役立つ
- コントラクトテストは、異なるタイムラインで2つのステップで行われるため、コンシューマとプロバイダの間の橋渡しをする中間モックサービスも採用される（このために[PACT](https://docs.pact.io/)というツールがあるらしい）
- コントラクトテストは機能テストではなく、マイクロサービス全体に対して定義され、実際の値を深くテストするよりも、入出力の形式や型に焦点を当て、サービスコールの入力と出力に必要な属性が含まれているか、レスポンスの待ち時間とスループットが許容範囲内に収まっているかを確認するもの
- コントラクトテストは、外部サービスの利用者に信頼感を与える一方で、 それらのサービスの保守者にとってはさらに価値のあるもの。あるサービスの利用者全員からコントラクトテストスイートを受け取ることで、利用者が影響を受けないことを確信した上で、そのサービスに対して安全に変更を加えることができる。

### End-to-end Testing 

- システムは、使用されているコンポーネントアーキテクチャに関係なく、ビジネス目標を満たすもの
- システム全体をブラックボックスとして扱い、GUIやサービスAPIなどのパブリックなインターフェイスを通して操作しながら、完全にデプロイされたシステム（モッキングの概念は厳密にはなし）で可能な限りテストを実施しなければならない
- マイクロサービスアーキテクチャでは、同じ動作でもより多くの可動部があるため、エンドツーエンドテストはサービス間のギャップをカバーすることで価値を発揮する
- しかし、エンドツーエンドテストの作成と保守は非常に困難
    - エンドツーエンドのテストは、他の方法よりも多くの可動部品を含んでいるため、失敗する理由も多くなる
    - エンドツーエンドのテストは、GUI やサービス間の非同期バックエンドプロセスなど、システムの非同期性を考慮しなければならない場合もある
    - これらの要因は、脆弱性、過剰なテスト実行時間、テストスイートのメンテナンスの追加コストにつながる可能性がある
- エンドツーエンドテストの複雑性を管理するのに役立つガイドライン
    - エンドツーエンドテストはできるだけ少なく書く
    - ペルソナとユーザージャーニーに焦点を当てる
    - エンドを賢く選択する
    - infrastructure-as-codeによる再現性の確保
    - データに依存しないテストにする

## まとめ

5つのテスト戦略について見てきましたが、全てを実行すればマイクロサービスアーキテクチャ特有のサービス間のやりとりについて、かなり厳密にテストができそうです。特にコントラクトテストについては、サービスに破壊的な変更を加えてしまうことを事前に防ぎ、安全に変更を加えられるという点でとても有効だと感じました。（これこそモノシリックなアーキテクチャにはない新しい観点なのかもと感じました。）

一方で、多数のチームに分かれて、それぞれが別々のサービスを担当する場合には、例えばコントラクトテストは非常に重要だと思いますが、ある程度横断してサービスを担当している場合には少し重要度は下がるのかなと思いました。そのような意味で、それぞれのテストの重要度やどこまでやるべきかといった点はケースバイケースで判断が必要そうだなと感じています。

具体的なテストの書き方や、5種類のテストを誰がいつ実行するのかなど、実践するにあたりイメージがついていない部分もあるので、また別の機会に調査してみたいと思います。


### 参考

- [Testing Strategies in a Microservice Architecture](https://martinfowler.com/articles/microservice-testing/)
- [Best testing strategies in a Microservice architecture](https://www.cigniti.com/blog/microservices-architecture-testing-strategies/)
- [Most Popular Microservice Testing Strategies](https://newizze.com/most-popular-microservice-testing-strategies/)
- [Microservices Testing Strategies, Types & Tools: A Complete Guide](https://www.simform.com/blog/microservice-testing-strategies/)