オブジェクトの広場はオージス総研グループのエンジニアによる技術発表サイトです

コンテナ・マイクロサービス

マイクロサービスアーキテクチャに効く!テスト技法

サービス間インターフェースのテスト技法 CDCテスト 概要編
オージス総研 技術部 アドバンストテクノロジセンター
今村 大輔
2021年4月22日

近年、ITシステムの開発においてマイクロサービスアーキテクチャを採用するケースが増えています。James Lewis / Martin Fowlerが提唱している通り、疎結合なサービスの組み合わせでシステムを構成することで様々なメリットをもたらします。一方で、従来のモノリシックアプリケーションの開発にはなかった難しさがあり、プロジェクトを円滑に推進するためには相応のノウハウが必要です。特にテストについては「やり方」を事前整備しておかないと、品質不良やスケジュール遅延につながりかねません。この不定期連載では、マイクロサービスアーキテクチャを高精度・高生産性で実現するテスト技法をご紹介します。
第1回目の今回は、サービス間のインターフェース仕様をテストする「CDCテスト」の概要をご紹介します。

マイクロサービスアーキテクチャとインターフェース仕様の課題

従来のモノリシックアプリケーションに比べて、マイクロサービスアーキテクチャによる開発・運用で難しくなるポイントが、サービス間のインターフェースです。単純にサービスを分割すればするほどインターフェースの数が増えます。多数のサービスが同時並行でエンハンス開発され続けると、最新インターフェース仕様への追従や下位互換を保つための作業が雪ダルマ式に膨張します。 また、サービス間を実際に結合して試験するためには、互いに実装がある程度成熟している必要があります。よって、試験できるのはプロジェクトの終盤戦。問題を検出することができても、本質的対応は取れない場合が多いのではないでしょうか。

CDCテストはこのような課題を解決するために考案されたテスト技法です。

CDCテスト(Consumer Driven Contract Test)のコンセプト

CDCテスト(Consumer Driven Contract Test)をざっくり表現すると、

  • サービス間連携のインターフェースをTDDするテスト技法です
  • コンシューマ主導で作成される「Contract」と呼ばれる入出力データの定義からコンシューマ/プロデューサのスタブとテストコードを自動生成します
  • プロデューサはContractを元にCI(自動回帰試験)することで、都度の変更がコンシューマに影響しないことを判断・保証します。コンシューマもまた同様です。
  • 「Contract」はコンシューマ/プロデューサで共有します。GitHub等で管理し、Pull Requestで仕様変更を通知・記録します

です。以下、成果物とワークフローの2つの視点から詳しく説明します。

CDCテストの概要(成果物視点)

まず、成果物の視点です。

CDCテストの成果物

コンシューマのサービスA(の開発者)とプロデューサのサービスB(の開発者)がいて、サービスBが「○○管理API インターフェース仕様」を作成し、サービスAと合意形成する点までは従来のシステム間連携と変わりありません。

CDCテストでは、「○○管理API インターフェース仕様」を具体化する形で「○○管理API Contract」をサービスAが作成します。この「○○管理API Contract」は、サービスAがサービスBを使用する上で、入力値のパターンと期待される出力値を契約(定義)したものです。具体的には、入力のHTTPリクエストが

  • メソッドは“HTTP/GET”
  • パスは“/users”
  • クエリ文字列でキーが"id"、値が"machidamachizo"

であれば、出力のHTTPレスポンスが

  • ステータスコードが"200"
  • Content-Typeが"application/json"
  • ボディのname属性が"町田町蔵"
  • ボディのorganization属性が"INU"

である、といったことを取り決めたものになります。JUnitに触れたことがあればすぐにピンとくるでしょう。この定義をもとにTDDや回帰試験の自動化を実現します。

入出力するデータとプロトコルを定義しただけでは、これまでのテスト仕様と大差ありません。CDCテストでは一歩進めて、「○○管理API Contract」からコンシューマとプロデューサそれぞれのスタブやテストコードを自動生成します。ツールや実装言語によりますが、概ね下記のようなものが出力されます。

コンシューマの内部結合試験に使用するもの プロデューサの内部結合試験に使用するもの

プロデューサスタブ

  • 実物のプロデューサの代替として使用する簡易なWEBサーバ
  • Contractで定義されたHTTPリクエストを受信すると、対応するHTTPレスポンスを返す

コンシューマスタブ

  • 実物のコンシューマの代替として使用するテストドライバ
  • 実体はBDD形式のテストコード
  • Contractで定義されたHTTPリクエストを送信し、返ってきた実際のHTTPレスポンスとContractで定義された期待値を比較する

同一のContractから自動生成することで、プロデューサ/コンシューマ互いに同じデータでテストを実施できます。想定外のデータが入出力されてしまうリスクをヘッジできるでしょう。データを入力する側であるコンシューマ主導でContractを作成することで、必要十分なデータパターン網羅が期待できます。また、ツールによってはHTTPプロトコルだけでなく、非同期メッセージングなどの高度なプロトコルにも対応しています。

CDCテストの概要(ワークフロー視点)

次に、ワークフローの視点です。

CDCテストは単純に品質保証として入力に対する出力が仕様通りかを判定するだけではありません。もう少し広い意味で、サービス間連携のインターフェースにまつわる課題を解決する手法です。Contractをコンシューマ/プロデューサで共有することで、開発のワークフローを改善します。

ワークフローの何が良くなるかを端的に述べると、

  • コンシューマ/プロデューサがそれぞれ独立して開発を進められ、仕様更新を都合の良いタイミングで取り込むことができる
  • コンシューマ/プロデューサが意図せず相手に影響する変更を加えていないか、自身のみで検出・判断できる

ということです。

シナリオ形式で具体的に説明します。下図はサービスAとサービスBが並行して開発を実施する場合を表現したものです。

CDCテストのワークフロー

計画段階/スプリント#1/スプリント#2/スプリント#3、それぞれのタイミングでCDCテストがどのように作用するか、順を追って説明します。

計画段階 進め方の違い

モノリスの基幹システムから顧客向けサービスをストラングラーパターンでマイクロサービスに切り出すとしましょう。下記のサービスを連携させようとしていますが、それぞれのサービス特性から進め方の足並みがそろいません。

  • サービスA(コンシューマ):顧客向けサービスを担うフロントシステム
    • スクラムで開発を推進
    • スプリント毎にフィードバックを受けながらUIをブラッシュアップする進め方
    • サービスBが管理するデータをREST API経由で参照・更新する
  • サービスB(プロデューサ):基幹システム
    • ウォーターフォールで開発を推進
    • 影響調査や入念なテストを手堅く積み重ねる進め方
    • サービスA向けにREST APIを新たに追加・公開する

言うまでもなく、このような「進め方の違い」はスケジュールや品質に影響します。このような場合、サービスBはインターフェース仕様を先行提供し、サービスAは早期にサービスBのスタブを実装して「動くようにする」のがセオリーです。しかし、現実はそう単純ではありません。

  • サービスA側のレビューでデータ桁数不足が判明
  • サービスBがテストした結果、エラーコードが変わる

といったことが起こり得ます。仕様変更のリスクを互いに抱えることになります。

CDCテストではContractをGitHub等の共用リポジトリで管理し、CIで内部結合試験を自動実行することでヘッジします。

スプリント#1 Contractによるデータパターンの合意形成

スプリント#1でContractを作成すれば、自動生成されるサービスBのスタブを用いてUIとの内部結合を実施できます。サービスBがContractのPull RequestをMergeすれば、入出力するデータパターンが合意形成されたことになります。サービスAのユーザレビューの結果、スプリント#2の序盤でContractを更新することになっても安心です。スタブを逐一メンテナンスしなくても、自動生成で最新化することができます。また、Pull RequestとMergeを通じて確実にサービスBとコミュニケーションできます。

スプリント#2 Contractによる仕様変更の影響調査

スプリント#2の中盤、サービスBがテストで不具合を検出し、当初予定していたエラーコードを返せないことが判明しました。別のコード値を使用しなければなりません。

もし、そのような場合でもサービスAへの影響調査は不要です。まずはサービスBの実装を手早く修正し、Contractによる内部結合試験を再実行してみましょう。サービスAで当初予定していたコード値が使われていれば(Contractで定義されていれば)エラーが検出されますし、使われていなければテストを通過します。

内部結合試験を再実行した結果、当初予定のコード値が使われていることが判明したとします。Contractをプロデューサから更新しなければなりません。

そんな場合でも、gitのブランチを分けておけば、サービスAは任意のタイミング(スプリント#3冒頭)で変更をMergeできます。これにより、スプリント#2の最後、ユーザレビュー直前でスタブを差替えるという高リスク作業を回避できます。もちろん、サービスB側は最新のContractで作業を進められます。もしも、サービスAが(スタブではなく)サービスB開発サーバを直接参照していた場合、反映タイミングの調整交渉が発生していたでしょう。

スプリント#3 スムーズな外部結合試験

そして、スプリント#3中盤の外部結合試験で実際にサービスAとサービスBを結合します。互いにContractで定義した同じデータを用いて内部結合試験済みであるため、想定外のデータが入出力されるリスクを大きく下げられます。Contractで同値クラス毎の入出力が合意されていれば、必然的にコンシューマとプロデューサでデータパターン認識がそろいます。認識外データのチェックロジック不備などは内部結合試験段階で潰せるでしょう。互いに合致したデータを入出力できていれば、連携する対向サービスが実物かスタブかは大きな問題ではありません。いざ外部結合試験で実物同士を接続しても、スムーズに試験できるでしょう。

CDCテストのメリットと効果

スタブ・テストコードの自動生成

メリットとしてわかりやすいのはスタブ・テストコードの自動生成です。コンシューマ/プロデューサがそれぞれ別個にスタブ・テストコードを実装するの比べ、労力は格段に下がります。1つのContractでコンシューマ/プロデューサ双方のテストが賄えて、まさしく一石二鳥です。

データパターンの明確化

さらに、インターフェース仕様だけではわからない具体的なデータパターンが明確になるのも大きなメリットです。例えば、エラーコードなどは基本設計で完全に洗い出すのは難しいものです。詳細設計や実装で例外ケースが明らかになる都度、追加・更新・削除される場合がほとんど。その際にコンシューマが条件分岐に利用している/していないがわかれば、不要な処理を外すなど設計・実装を最適化できます。

変更差分の機械的検出

そして何より、コンシューマ/プロデューサ双方の変更差分を機械的に検出できるのもメリットです。ContractはGitHubなどで差分管理され、Pull Requestによりコミュニケーションの証跡が残ります。Pull Requestされたブランチを自動テストにかけることで、影響有無を判断することもできます。従来、Excelで記述されたインターフェース仕様書を目視で比較して差分検出していたのに比べると、コミュニケーションの質が各段に向上します。

外部結合試験の作業負荷低減

効果の面でも、外部結合試験の作業負荷が大きく下がります。前述の通り、コンシューマ/プロデューサは互いに同一データで内部結合試験済みです。テスト観点は必然的に通信経路など基盤レイヤとの結合に絞られます。テストケースを削減できる上、試験環境占有や作業立合調整といった雑事も楽になります。そして何よりも、「フタをあけてみないとわからない」怖さから解放され、心に余裕をもって作業に臨むことができます。

CDCテスト導入の勘所

CDCテストを実際のプロジェクトへ適用する際、乗り越えなければならない壁が2つあります。基本的なテスト技法と思考法を使いこなすこと、連携先サービスを巻き込むことです。

基本的なテスト技法と思考法を使いこなす

インターフェース仕様からContractを定義するのは前述の通りですが、その際に重要になるのが同値分割・境界値分析、デシジョンテーブルといったデータパターンの洗い出しです。手法それ自体は昔からある確立されたメソッドですが、実践にあたっては業務(外部仕様)についてのしっかりした理解が必要です。さらに、まだまだ手探り部分が残るであろう基本設計段階で実践的なデータパターン抽出をやるとなると、仕様書をかなり批判的に(クリティカルに)読み解かなければなりません。

連携先サービスを巻き込む

言わずもがなですが、CDCテストはコンシューマ/プロデューサのどちらか一方だけで完結するものではありません。片側でスタブを自動生成するだけでも多少の効果はありますが、Contract(契約)としてデータパターンを合意形成し、互いに独立して内部結合試験することで生産性と品質を向上させるのが目的です。 何ら下地のないところで、いちサービスの開発者からボトムアップで適用を働きかけるのは難しいかもしれません。サービスを横断して技術面からシステム全体の品質向上を担うSET(Software Engineer in Test)のようなロールが、ある種のトップダウンで適用を働きかけることが重要ではないでしょうか。

CDCテストを支援するツール

CDCテストを支援するツールとして、2021/03現在は「Pact」「Spring Cloud Contract」の2つが有名です。それぞれをざっくり比較すると、以下の特徴があります。

比較ポイント Pact Spring Cloud Contract 備考
Contractの記述形式
  • RSpec
  • JUnit
  • ・・・
  • YAML
  • Groovy

PactはRSpec等でContractを記述し、Pactファイルと呼ばれる中間データを出力して実行する

Spring Cloud ContractにはPactファイル互換出力機能あり

対応言語
  • 基本形はRuby
  • 各種言語のアドオンあり
  • 基本形はJava
  • プロトコル越しにテストする場合、言語には依存しない
-
対応プロトコル
  • HTTP
  • 一部言語のみ「Asychronous message pacts」のサポートあり
  • HTTP
  • JMS
  • Kafka
  • ・・・
  • その他、任意のプロトコルに対応する拡張ポイントあり
-
Contractをサービス間で共有する仕組み
  • Pact Broker
  • Mavenリポジトリ
-
採用実績
  • 具体的な採用事例は見当たらず
-

それぞれ利点がありますが、Spring Cloud ContractはJava・Springのエコシステムと親和性が高く、YAMLでContractを書ける点がエンタープライズ系の開発に適合しそうです。実装言語がサービス毎に異なる場合にはPactの各種言語アドオンが魅力です。

詳細な解説は次回に譲りますが、Spring Cloud ContractのYAMLでREST APIのContractをサンプル実装すると以下のような形になります。

description: This contract verifies User API! Please built in your CI system consumer and provider each other!
name: User API Contract Test Case 01-01
ignored: false
request:
  url: /userservice/user
  queryParameters:
    id: machidamachizo
  method: GET
  headers:
    Accept: application/json
response:
  status: 200
  headers:
    Content-Type: application/json
  body:
    id: machidamachizo
    name: 町田町蔵
    organization: INU

定義している内容はいたってシンプルです。

  • テストの定義
    • テスト内容の説明
    • テストケース名
  • 入力値の定義(HTTP要求の内容)
    • URL
    • クエリ文字列
    • メソッド
    • HTTPヘッダ
    • HTTPボディ
  • 出力値の定義(HTTP応答の内容)
    • ステータスコード
    • HTTPヘッダ
    • HTTPボディ

HTTPプロトコルを理解していれば、初見でもほぼ理解できるでしょう。初心者でもすぐにテスト実施できるのではないでしょうか。プロトコルが非同期メッセージングの場合でも概ね同様の記述形式です。

上記のContractからコンシューマスタブを自動生成すると、以下のようなJUnitのテストコードが出力されます。

public class UserApiTest extends UserApiBase {

    @Test
    public void validate_user_API_Contract_Test_Case_01_01() throws Exception {
        // given:
            RequestSpecification request = given()
                    .header("Accept", "application/json");

        // when:
            Response response = given().spec(request)
                    .queryParam("id","machidamachizo")
                    .get("/userservice/user");

        // then:
            assertThat(response.statusCode()).isEqualTo(200);
            assertThat(response.header("Content-Type")).isEqualTo("application/json");

        // and:
            DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
            assertThatJson(parsedJson).field("['id']").isEqualTo("machidamachizo");
            assertThatJson(parsedJson).field("['name']").isEqualTo("\u753A\u7530\u753A\u8535");
            assertThatJson(parsedJson).field("['organization']").isEqualTo("INU");
    }
}

given/whenのブロックでContractの入力値定義を設定し、プロデューサへHTTP要求を送信します。then/andのブロックで実際にHTTP応答された値とContractの期待値を突き合わせて検証しています。

まとめ

「マイクロサービスアーキテクチャに効く!テスト技法」の第1回目として、CDCテストの概要を説明しました。CDCテストは、コンシューマ主導で作成するContractからコンシューマ/プロデューサのスタブを自動生成してサービス間の連携をTDDし、Contractの更新をPull Requestして破壊的変更を検知するテスト技法です。 サービス間のインターフェースが増加するマイクロサービスアーキテクチャでの適用はもちろんのこと、従来のモノリシックアプリケーションでも外部インターフェースのテストとして有効な技法です。ぜひ、適用を検討してみてください。

次回・次々回は「実践編」として、Spring Cloud Contractを用いてCDCテストを実施してみましょう。現実的、かつ、応用的なサービスの実装を想定したトレーニングアプリケーションをお届けできる見込みです。ご期待ください。