マイクロサービスアーキテクチャへのIntegration Test導入のすゝめ
こんにちは、バックエンドを中心に開発をしています、野島といいます。
ソフトウェアテスト自動化カンファレンス2023に「マイクロサービスアーキテクチャへのIntegration Test導入のすゝめ」というお題で登壇しました。
そちらで発表した内容を記事にしつつ、当日話しきれなかった内容についても書きます。
発表は下記の内容を話しました。
- Integration Testの導入を決意した背景にあった課題
- Integration Testの導入/運用での工夫
- Integration Testを導入して得られたメリット
- まとめ
本記事では、Integration Testを以下の定義で扱います。
マイクロサービスが依存する外部コンポーネントをモック化せずに行うAPIテスト。
外部コンポーネントとは、具体的にはデータストア、外部サービス、テスト対象が依存するマイクロサービス、などを指します。
テスト対象のマイクロサービスが公開するAPI(WebAPI等)を介して自動テストを行います。
Integration Testの導入を決意した背景にあった課題
大きくわけてふたつの課題が存在していました。
順を追って説明していきます。
- E2Eでカバーしきれない範囲に重要な機能が存在する
- マイクロサービスアーキテクチャの難しさ
E2Eでカバーしきれない範囲に重要な機能が存在する
弊社では在庫管理プロダクトを開発していますが、次の重要な機能が存在します。
- 在庫数の推定: IoT重量計を用いて、重量ベースで在庫数を算出
- 在庫の自動発注: 在庫数の変動に応じて自動発注を行う
在庫数の推定: IoT重量計を用いて、重量ベースで在庫数を算出
直感的にわかるようにgifを利用して説明します。
本をIoT重量計に乗せると、重量を計測し在庫数を算出、画面上に表示されます。
在庫数の推定は、重量を計測するIoT重量計のリクエストから始まります。
リクエストで送信された重量を利用して在庫数がどれだけあるか、という結果を算出します。
このgifの場合、本を4冊IoT重量に載せて、その重量から在庫数が4という結果が得られました。
この機能は、IoTデバイスがトリガーとなる処理です。
そのため画面トリガーでテストするような一般的なE2Eテストのカバー範囲外になります。
在庫の自動発注: 在庫数の変動に応じて自動発注を行う
画像の左側のグラフは、青い線が在庫数の変動を表しています。
黄色い線が閾値、発注点を表しています。
在庫数を示す青い線が、閾値を示す黄色い線を下回った場合、自動で発注する機能が存在します。
自動発注がおこなわれると、画像の右側のようなメールが送信され、発注処理がおこなわれます。
この機能は、Cronで定期的に実行されるJOBで行われる処理です。
そのため画面トリガーでテストするような一般的なE2Eテストのカバー範囲外になります。
紹介した機能が、弊社のプロダクトでは重要なものになっているのですが、これらがE2Eでカバーしきれない範囲に存在することに課題感がありました。
マイクロサービスアーキテクチャの難しさ
弊社では、マイクロサービスアーキテクチャを採用しています。
マイクロサービスアーキテクチャでは、サービス間の通信が発生し、ひとつのサービスで処理が
完結しないことがあり、そこが難しさのひとつです。
E2Eでカバーしきれない範囲に重要な機能が存在するで紹介した、在庫推定、自動発注の機能ですが、これらには複数のサービスが関わります。
複数のサービスが依存に現れてくると、単一のサービスの一部のロジックにUTを書いても、サービス間を繋げて動作させた時に本当に正しく動くかについて自信を持つことが難しいです。
Integration Testを導入する前に、先行してUT,E2Eが存在しましたが、これら重要機能が既存のテストでカバーできていない状態でした。
重要機能がテストでカバーしきれていないことが、デプロイの不安要素になっていました。
弊社では開発生産性をあげよう、デプロイ頻度を上げよう、という取り組みがあり、デプロイ頻度を上げるには不安要素を取り除く必要がありました。
そこで、Integration Testを導入することにしました。
Integration Testの導入/運用での工夫
Integration Testの導入/運用での工夫について紹介します。
- テストケースに優先度をつけて実装を進める
- テスト範囲を柔軟に設定し、複数のテストを組み合わせる
上述は、登壇時に話した内容で、さらに次の内容を追加して紹介します
- テストの並列実行
- 2種類のテストのretry
- JOBのロジックをテストAPIで公開
テストケースに優先度をつけて実装を進める
弊社はスタートアップ企業であり、機能開発により工数をかけたいという事情があります。
テストの実装にかけられる時間には制限がありました。そこで、テストで守るべき重要な機能と、考えられるテストケースをリストアップし、優先度をつけて実装を進めました。
テストケースの選定、優先度づけは、開発生産性をあげるための有志のメンバーからなる委員会で協議しながら進めていきました。
テスト範囲を柔軟に設定し、複数のテストを組み合わせる
テストが依存するサービスを増やすと、テストの不安定さが増していきます。
在庫推定の機能と、自動発注の機能について、すべての関わるサービスを繋いでテストを実装することを最初は考えましたが、上述の理由でやめました。
代わりに在庫推定の範囲と、自動発注の範囲でテストを分けて実装しました。
これらを組み合わせるとあたかもすべてを繋いでテストを実装したかのようになります。
適切な粒度でテスト範囲を分割して実装することで、テストの不安定さを抑えることができます。
テストの並列実行
テストの実装が充実するにつれ、テストの実行時間が長くなっていきました。
また、実行時間が長くなるにつれテストの不安定さも増し、失敗する回数が増え、Flakyな状態になりました。
そこで、テストを並列実行することで、テストの実行時間の短縮と、安定性の向上を図りました。
いちばんテストの実行時間が長い時で20分強かかっていましたが、並列実行することで2-3分程に短縮することができました。
実はテスト実装しはじめて初期の段階でこの未来を予見していました。
そのため、悪影響を及ぼすようになったら、並列実行できるような形で実装していたので、難なく改修できました。
ITやE2Eを実装する場合には、並列化を見据えて実装することをオススメします。
2種類のテストのretry
テストケースのステップレベルのretryと、テストシナリオレベルでretryできるようにしています。
ステップレベルのretry
steps:
- protocol: http
request:
method: GET
url: http://example.com
expect:
code: OK
timeout: 30s # default values is 0, 0 means no timeout
retry: # default policy is never retry
constant:
interval: 5s # default value is 1s
maxRetries: 1 # default value is 5, 0 means forever
maxElapsedTime: 1m # default value is 0, 0 means forever
テストの実装には、scenarigo というYAMLでテストシナリオを記述するAPIテストツールを採用しています。こちらのツールには、retry機能 が存在しており利用しています。
(yamlの例文は readme.md から引用しています)
マイクロサービスアーキテクチャでは、非同期処理が登場しやすく、その処理結果をテストで確認したいことがあります。
そういった場合に、非同期処理の結果を待つのにretryが活躍します。
テストシナリオレベルのretry
ステップレベルでretryを入れても、どうしてもテストが不安定で失敗することがあります。
そのような時に、より大きい範囲でretryをかけることで、安定性を向上させることができます。
画像はテストで利用しているワークフローツールでretryをかけている様子です。
JOBのロジックをテストAPIで公開
こちらの記事で紹介している内容になります。
発注機能のJOBに対してテストを実装するにあたり、テストをしたい任意のタイミングでどのように発注処理を実行するかが課題でした。そこで、テスト用のAPIでJOBのロジックを公開し、これを呼び出すことでジョブと同じ処理を実行することにしました。
Integration Testを導入して得られたメリット
Integration Testを導入することで、重要な機能をテストでカバーできるようになりました。
それは安心感をもたらし、デプロイの不安要素を取り除くことができました。
デプロイ時に重要な機能が壊れていないという安心感があることで、デプロイ時に行っていた儀式ともよべるような作業をなくすことができました。
デプロイ後に本番環境で動作確認してみたり、デプロイ後はしばらくログを注視してみたり、と本当に効果があるのか怪しいような作業をしていました。
テストを導入することでこういった非効率な作業をなくすことができました。
結果として、デプロイのしやすさが上がり、現在では日に4,5回ほどデプロイを行っています。
デプロイ頻度が上がったことに関しては、テスト以外にもさまざまな取り組みをおこなっていたことが要因になっており、こちらの記事にまとめられています。
まとめ
Integration Testを導入するに至った課題感と、導入/運用上の工夫を共有しました。
UT,E2Eではテストしきれない範囲に重要な機能が存在することが課題としてありました。
導入/運用上の工夫としては、テストケースに優先度をつけて実装を進める、テスト範囲を柔軟に設定し、複数のテストを組み合わせる、テストの並列実行、2種類のテストのretry、JOBのロジックをテストAPIで公開という5つの工夫を紹介しました。
(登壇時には最初のふたつを紹介しました)
結果として、重要な機能をテストでカバーできるようになり、デプロイ頻度・開発生産性の向上につながりました。
こちらで紹介した内容が、テスト戦略を考える際に参考になるものを提供できていれば幸いです。
登壇した感想
外部勉強会への登壇は私にとって初の機会でした。
15分の枠で発表したのですが、短い時間の中で話したいことをまとめることが難しかったです。
しかしながら、発表に関して質問、コメントをいくつか頂き、とても嬉しかったです。
E2Eでカバーできないところに重要な機能が存在する、というのはどうやら経験がある方が多かったようでコメントで盛り上がっているのを感じました。
また、デプロイ時の不安感が非効率な儀式とも呼べる作業につながりうる。というのも共感する方が多かったようです。
発表で上手く話せたか、という点を振り返るとできていない部分が多く、反省点が多いです。
次回、どこかに登壇できるような機会がある場合は、さらに上手に話せるように準備をして挑みたいです。
Discussion