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

マイクロサービス

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

サービス間インターフェースのテスト技法 CDCテスト 実践編その1 REST APIのテスト
オージス総研 技術部 アドバンストテクノロジセンター
今村 大輔
2021年5月26日

前回はサービス間のインターフェース仕様をテストする「CDCテスト」の概要をご紹介しました。
今回は、Spring Cloud Contractを用いてREST APIのCDCテストを実施します。あわせて、Spring Cloud Contractを実践活用するJavaプロジェクト構成例もご紹介します。

はじめに

Spring Cloud Contractを用いたCDCテストを習得するには、実装済みのトレーニングアプリケーションとContractを読解し、スタブ・テストコードを自動生成・実行してみるのが早道です。この記事を通じて、以下が実現できればと思います。

  • Contractを作成できるようになる
    • 記述形式
    • ディレクトリ配置ルール
  • Contractからテストコード・スタブを生成し、実行できるようになる
    • Gradleでテストコード・スタブ生成タスク実行
    • GradleでスタブをMavenリポジトリへインストール
    • Contractとテストコード出力内容の対応付け
    • (手作業で実装が必要な)テストコードのスーパークラスの内容把握
  • Spring Cloud Contractの設定箇所を把握する
    • Gradleの設定

なお、トレーニングアプリケーションで使用する各種ツール・ライブラリは以下のバージョンで実装・動作確認しています。

  • Spring Cloud Contract:2.2.5.RELEASE
  • Spring Boot:2.3.7.RELEASE
  • Gradle:6.7.1

トレーニングアプリケーションの開発・実行環境

ではさっそく、トレーニングアプリケーションのソースコードを見てみましょう。ソースコード一式はGitHubのリポジトリで公開しています。

GitHubからソースコードを取得する方法はこちらを参考にしてみてください。

開発・実行環境には以下を使用します。同等の環境を準備してください*1

構成 導入製品・バージョン インストールパス 備考
OS Windows 10 -
  • Mac, Linuxでも動作します。シェル・パス区切り文字・環境変数等、OS依存事項を適宜読み替えてください
Java仮想マシン JDK8
(Amazon Corretto 8で動作確認済)
C:\Program Files\Amazon Corretto\jdk1.8.0_275
  • JDK15以降では標準インストールされない(オプション扱い、別途jar追加要)クラスに依存しています。適宜、依存関係を調整ください
  • 環境変数「JAVA_HOME」への左記パス追加、環境変数「PATH」へのjavaコマンド追加を設定ください
IDE Eclipse Photon 2018 C:\Eclipse
  • 各プロジェクトのbuild.gradleよりGradleプロジェクトとしてインポートしてください
  • 未確認ですがVSCodeやIntelliJ Ideaでも動作すると思われます。Eclipse同様、各プロジェクトのbuild.gradleよりクラスパス等を構成ください
  • トレーニングアプリケーションではlombokを利用しています。必要に応じて、プラグインをインストールしてください
ワークスペース - C:\workspace
  • 以下、パスを記載する場合は本パスを基準に相対パスで記載します
  • 本パスにGitHubからトレーニングアプリケーションをcloneしてください

なお、開発PCはインターネットに接続できることが前提です。ビルド実行時、インターネットからSpring Cloud ContractやGradleなど各種ライブラリを自動ダウンロードします。

公開しているソースコードはご自身の責任の元でご利用頂くようお願いいたします。CDCテスト / Spring Cloud Contractの個人的学習を目的とするものであり、パッチ適用や脆弱性検査を含むメンテナンスは実施しておりません。本ソースコードに関する問題点及びその問題点が原因で発生した損害等に関して、オージス総研ならびにオブジェクトの広場編集部は一切責任を持ちません。

トレーニングアプリケーションの仕様・構成・ビルド・起動

今回のトレーニングアプリケーションは、以下3つのプロジェクトで構成されています。

プロジェクト 役割・用途・実装の概要 内容物
cdctest-rest-api
  • REST APIのインターフェース。「ユーザAPI」の定義など
  • Contract
  • コンシューマスタブ(テストコード)
  • Swagger Spec
cdctest-rest-producer
  • REST APIのプロデューサ(サーバサイド)。「ユーザAPI」を提供するHTTPサーバアプリケーション
  • Spring Bootで実装されており、WEBサーバ(Servletコンテナ)が内包されている
  • ユーザAPIの実装
cdctest-rest-consumer
  • REST APIのコンシューマ(クライアントサイド)。「ユーザAPI」を利用するコンソールアプリケーション
  • Spring Bootで実装されている
  • ユーザAPIクライアントの実装
  • テストコード(プロデューサスタブの起動を含む)

これらプロジェクトは、以下の仕様・配置で構成されます。

トレーニングアプリケーションの構成

各プロジェクトをビルドして、動作確認を行います。下記手順を実施してください。

  1. cdctest-rest-apiのビルドと実行

    1. demo-cdctest/cdctest-rest-apiディレクトリに移動
    2. 以下コマンドを実行してビルド

      gradlew build -x test
      
    3. 以下コマンドを実行してMavenローカルリポジトリへインストール

      gradlew install
      
  2. cdctest-rest-producerのビルドと実行

    1. demo-cdctest/cdctest-rest-producerディレクトリに移動
    2. 以下コマンドを実行してビルド

      gradlew build -x test
      
    3. 以下コマンドを実行してプロデューサ起動

      java -jar .\build\libs\cdctest-rest-producer-0.1.0-SNAPSHOT.jar
      
  3. cdctest-rest-consumerのビルドと実行

    1. demo-cdctest/cdctest-rest-consumerディレクトリに移動
    2. 以下コマンドを実行してビルド

      gradlew build -x test
      
  4. 動作確認

    1. demo-cdctest/cdctest-rest-consumerディレクトリに移動
    2. 以下コマンドを実行してコンシューマを実行

      java -jar .\build\libs\cdctest-rest-consumer-0.1.0-SNAPSHOT.jar
      
    3. 標準出力に下記が出力されてコンシューマ→プロデューサへHTTP通信が行われたことを確認

      2021-04-09 11:26:39.461  INFO 1404 --- [           main] j.c.o.r.r.c.r.c.ConsumerApplication      : try to execute http request: <GET http://localhost/userservice/user?id=root,[Accept:"application/json"]>
      2021-04-09 11:26:40.008  INFO 1404 --- [           main] j.c.o.r.r.c.r.c.ConsumerApplication      : received response! status code: 200 OK, response body:{"id":"root","name":"管理者","organization":"オージス総研"}
      

上記が一通り動作すれば、トレーニングアプリケーションの開発・実行環境は問題なく動作しています。

Contractの読み解き

Spring Cloud ContractのContractを具体的に読み解いていきましょう。「ユーザAPI」では、以下のContractを定義しています。

  • demo-cdctest/cdctest-rest-api/src/test/resources/contracts/UserApi/userapi-contract-testcase-01-01.yaml
description: This contract verifies User API! Please built in your CI system consumer and producer 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

Contractの基本構造は下記の通りです。HTTPプロトコルでやりとりする項目をストレートに表しています。

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

認証トークンやUUIDなど、自動採番されるため入出力値が固定できない場合もあるでしょう。その場合はrequest/response直下につくmatchersオブジェクトで値を定義してください。JSONPathで項目を指定し、正規表現で取り得る値の範囲を指定できます。

response:
  matchers:
    body:
      - path: $.message
        type: by_regex
        value: ^(?!\\s*$).+

Contractのスキーマ詳細は公式ドキュメントを参照してください。ここでは頻繁に使用する属性を紹介します。

属性 データ型 説明 備考
description 文字列
  • 何をテストするContractであるか説明する項目
  • コンシューマスタブ・プロデューサスタブに反映されないコメント欄
プロジェクトで記載内容をテンプレート化し、後々に向けた内容整理をしておくとよい。Contractの数が増えると、テストの意図がわかりにくくなる
name 文字列
  • テスト名
  • 本項目がコンシューマスタブのテストメソッド名になる
日本語使用可能
ignored 真偽
  • 有効/無効を切り替えるフラグ
  • trueに設定した場合、コンシューマスタブのテストメソッドに@Ignoreが付与され実行がSKIPされる
-
request url 文字列
  • REST APIのホスト名以下のパスを定義する
ホスト名・ポート番号は環境毎に異なるため、コンシューマスタブ・プロデューサスタブで設定する
queryParameters ハッシュの配列
  • クエリ文字列での入力値を定義する
-
method 文字列
  • HTTPメソッドの定義
-
headers ハッシュの配列
  • リクエストヘッダでの入力値を定義する
-
body オブジェクト
  • リクエストボディでの入力値を定義する
YAMLのオブジェクトで定義した内容が、コンシューマスタブ・プロデューサスタブではJSONに展開される
matchers オブジェクト
  • 固定できない入力値を定義する
  • 正規表現や出来合いのパターン(only_alpha_unicode等)を指定可能
URL、リクエストヘッダ、リクエストボディ等でオブジェクト構造(値範囲定義の仕方)が異なる。詳細は公式ドキュメント参照
response status 整数
  • ステータスコードの定義
-
headers ハッシュの配列
  • レスポンスヘッダでの出力値を定義する
-
body オブジェクト
  • レスポンスボディでの出力値を定義する
YAMLのオブジェクトで定義した内容が、コンシューマスタブ・プロデューサスタブではJSONに展開される
matchers オブジェクト
  • 固定できない出力値を定義する
  • 正規表現や出来合いのパターン(only_alpha_unicode等)を指定可能
レスポンスヘッダ、レスポンスボディ等でオブジェクト構造(値範囲定義の仕方)が異なる。詳細は公式ドキュメント参照

また、Contractが配置されたパスにも注目してください。

demo-cdctest\cdctest-rest-api\src\test\resources\contracts\UserApi

Contractを配置するパスはSpring Cloud Contractの規約で定められています。

  • (プロジェクトのディレクトリ)/src/test/resources/contracts

上記がSpring Cloud Contractが使用するリソースディレクトリです。上記直下にAPIを分類するディレクトリ(上記例では「UserApi」)を掘って、それぞれにContractを配置します。このディレクトリ名をもとに自動生成されるコンシューマスタブのクラス名が決まります。試験対象となるインターフェースを正しく表現した名称にするとよいでしょう。

コンシューマスタブ(テストコード)の生成

次に、コンシューマスタブ(テストコード)を自動生成してみます。 demo-cdctest/cdctest-rest-apiディレクトリに移動して、下記のコマンドを実行してください。

gradlew generateContractTests

同プロジェクトに含まれるContractから、コンシューマスタブが生成されます。今回のトレーニングアプリケーションの場合、demo-cdctest/cdctest-rest-api/src/test/gen-java/jp/co/ogis_ri/rd/nautible/cdctest/rest/contracttest/UserApiTest.javaが出力されたことと思います。

  • demo-cdctest/cdctest-rest-api/src/test/gen-java/jp/co/ogis_ri/rd/nautible/cdctest/rest/contracttest/UserApiTest.java
public class UserApiTest extends UserApiBase {

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

        // when:
            ResponseOptions 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");
    }
        ・・・(中略)・・・
}

@Testアノテーションが示す通り、JUnitのテストコードが生成されます。「given:」「when:」でコメントされたブロックにて、Contractで定義したHTTPリクエストのパラメータが埋め込まれているのがわかります。また、「then:」「and:」でコメントされたブロックにて、実際のHTTPレスポンスの結果とContractで定義した期待値をassertしているのがわかります。

そして、UserApiBaseクラスを継承している点にも注目してください。このクラスでは、プロデューサのホスト名やポート番号といったテスト実行時の接続情報やJUnitからSpringの設定を読み込むためのアノテーションを設定します。このクラスは、自身で実装する必要があります。自動生成はされません。Spring Cloud Contractを利用するプロジェクト側固有の内容であるためです。テストデータを事前準備する場合や環境ごとに起動構成を変更する場合は本クラスで実装することになります。

  • demo-cdctest/cdctest-rest-api/src/test/java/jp/co/ogis_ri/rd/nautible/cdctest/rest/contracttest/UserApiBase.java
@ExtendWith(SpringExtension.class)
@SpringBootTest(classes= {UserApiBase.class})
public class UserApiBase {

    @LocalServerPort
    int port;

    @BeforeEach
    public void setup() {
        RestAssured.baseURI = "http://localhost";
        RestAssured.port = this.port;
    }

}

こちらで読み解くべきポイントは以下になります。詳細はSpring BootRestAssured(REST APIのテスト支援ライブラリ)のJavadocを参照してください。

  • @ExtendWith(SpringExtension) でJunit5とSpringを組み合わせて利用することを設定
  • @SpringBootTest でSpring Bootのテストであることを設定
  • @LocalServerPort で src/test/resources/application.propertiesのlocal.server.port設定値をプロデューサのポート番号とする(ここでは80番ポートを使用しています)
  • @BeforeEachのメソッドで各テストメソッド実行前にプロデューサのホスト名・ポート番号をRestAssuredへ設定する

プロデューサの内部結合試験

では、実際にコンシューマスタブを実行してプロデューサの内部結合試験を実施してみましょう。コンシューマスタブとなるcdctest-rest-apiからプロデューサであるcdctest-rest-producerへHTTP通信が行われます。

プロデューサの内部結合試験

プロデューサを起動した状態で、demo-cdctest/cdctest-rest-apiディレクトリに移動して、下記のコマンドを実行してください。(Eclipseから実行する場合は、自動生成されたUserApiTestを「JUnitテスト」で実行してください)

gradlew test

プロデューサの標準出力から、Contractに基づくHTTPリクエストが送られてきたことがわかります。

2021-04-09 12:05:50.158  INFO 5104 --- [p-nio-80-exec-4] j.c.o.r.r.c.r.p.UserApiController        : received request... id:machidamachizo
2021-04-09 12:05:50.167  INFO 5104 --- [p-nio-80-exec-4] j.c.o.r.r.c.r.p.UserApiController        : requested user is found... User(id=machidamachizo, name=町田町蔵, organization=INU)

また、Gradleのテスト結果レポートがdemo-cdctest\cdctest-rest-api\build\reports\tests\test\index.htmlに出力されます。HTTPレスポンスとContractの期待値が合致していることがわかります。

プロデューサスタブの起動

次に、プロデューサスタブを起動します。プロデューサスタブの実体はContractを内包したスタブjarファイルとStub RunnerというSpring Cloud Contractのツールです。前述した通り、cdctest-rest-apiをビルドするとContractを内包したスタブjarファイルが生成されます。それをMavenリポジトリへインストールし、Stub Runnerから参照します。

プロデューサスタブには2通りの起動方法があります。みなさんのプロジェクトに適合する方法を選んでください。

起動方法 概要 適用すべきケース 備考
JUnitテストコードへの埋め込み
  • JUnitにStub Runnerを起動するアノテーションを付与し、テスト実行時に自動起動する
  • Javaで実装されたアプリケーション (特に、Spring Frameworkを適用している場合)
  • JUnitからコンシューマを(簡単に)起動できるアプリケーション
JUnitテストコードがSpring Bootに依存します。
プロデューサスタブ(JUnitテストコード)とコンシューマを同一JVMで実行する場合、依存ライブラリ構成によっては正常動作しない可能性があります。
Stub Runner Boot Application実行
  • Stub Runner Boot Applicationというプロデューサスタブ実行用のスタンドアロンアプリケーションを起動する
  • JUnitから実行し難い場合 (特に、Spring Frameworkと競合するライブラリ構成の場合)
  • Java以外の言語で実装されたアプリケーション
コンシューマとは完全に別のプロセスで動作するため、様々なアプリケーションで確実に適用できます。

なお、さきほど起動した(実物の)プロデューサは停止しておいてください。起動したままだと、ポート番号が重複してスタブが起動しません。

プロデューサスタブをJUnitテストコードへ埋め込んで使用する

まず、JUnitテストコードへの埋め込みのケースから説明しましょう。プロデューサスタブを埋め込んだコンシューマのテストコードをcdctest-rest-consumerプロジェクトに用意しています。

  • demo-cdctest/cdctest-rest-consumer/src/test/java/jp/co/ogis_ri/rd/nautible/cdctest/rest/consumer/ConsumerApplicationContractTest.java
@ExtendWith(SpringExtension.class)
@AutoConfigureStubRunner(
        stubsMode = StubRunnerProperties.StubsMode.CLASSPATH,
        ids = "jp.co.ogis_ri.rd.nautible.cdctest:cdctest-rest-api:0.1.0-SNAPSHOT:stubs:80"
        )
@SpringBootTest
public class ConsumerApplicationContractTest {

    @Test
    public void test001_200_OK() throws Exception {
        assertDoesNotThrow(() -> ConsumerApplication.main("machidamachizo"));
    }

}

ここで読み解くべきポイントは@AutoConfigureStubRunnerアノテーションです。これは、Spring Cloud Contractが提供するアノテーションでスタブjarの参照とStub Runnerの起動を設定するものです。詳細な設定項目は公式ドキュメントを参照ください。ここでは重要項目に絞って設定を紹介します。

設定項目 項目概要
stubsMode
  • スタブjarをどこから読み込むかを指定する
  • 設定可能な値は以下
    • StubRunnerProperties.StubsMode.CLASSPATH:JUnit実行時のクラスパス
    • StubRunnerProperties.StubsMode.LOCAL:実行ノードのローカルMavenリポジトリ
    • StubRunnerProperties.StubsMode.REMOTE:リモートMavenリポジトリ
ids
  • 読み込むスタブjarを指定する
  • 「:」区切りで、以下を指定する
    • グループID
    • アーティファクトID
    • バージョン
    • パッケージング
    • ポート番号

stubsModeについて、トレーニングアプリケーションではスタブjarをクラスパスから読み込んでいます。テスト実行時、どのライブラリをクラスパスに含めるかはgradleで管理するのが望ましいと筆者は考えています。プロジェクトの規模やContractの構成管理ルール次第では、ローカルやリモートを指定するほうが好都合かもしれません。

idsについて、Mavenに馴染みがない方は戸惑ってしまうかもしれません(逆に、馴染みがあれば一発でわかると思います)。馴染みが無い方向けにフォローすると、MavenはグループID・アーティファクトID・バージョン番号・パッケージングの各項目を組み合わせてjarを一意に特定します。トレーニングアプリケーションではバージョンを明示的に指定していますが、「+」を指定するとMavenリポジトリ内の最新バージョンを読み込みます。慣行上、Spring Cloud Contractではスタブjarのパッケージングに「stubs」を使用します。

プロデューサスタブをStub Runner Boot Applicationで実行する

次に、Stub Runner Boot Application実行のケースを説明します。Stub Runner Boot ApplicationはStub Runnerをスタンドアロンで実行するSpring Bootアプリケーションです。このアプリケーションの引数にスタブjarやポート番号を指定し、プロデューサスタブを起動します。

Stub Runner Boot Applicationをダウンロードし、cdctest-rest-api/libへ配置してください*2。そして、demo-cdctest/cdctest-rest-apiディレクトリに移動して、下記のコマンドを実行してください。

java -Dstubrunner.ids="jp.co.ogis_ri.rd.nautible.cdctest:cdctest-rest-api:0.1.0-SNAPSHOT:stubs:80" -Dstubrunner.stubsMode="LOCAL" -jar .\lib\spring-cloud-contract-stub-runner-boot-2.2.5.RELEASE.jar

Mavenローカルリポジトリより、cdctest-rest-api-0.1.0-SNAPSHOT-stubs.jar(cdctest-rest-apiのスタブjar、Contractを内包している)がロードされ、プロデューサスタブが起動します。プロデューサスタブが読み込んだContractの一覧は http://localhost/__admin/ で参照できます。

javaコマンド引数の「-Dstubrunner.ids」「-Dstubrunner.stubsMode」はJUnitテストコードへ埋め込んで使用する際の@AutoConfigureStubRunnerアノテーションと同様です。「-jar」引数でStub Runner Boot Applicationのjarファイルを指定します。

コンシューマの内部結合試験

では、実際にプロデューサスタブを実行してコンシューマの内部結合試験を実施してみましょう。コンシューマからプロデューサスタブとなるcdctest-rest-apiへHTTP通信が行われます。プロデューサスタブはコンシューマを呼び出すテストコードに埋め込まれており、テストコード実行の都度起動します。

コンシューマの内部結合試験

demo-cdctest/cdctest-rest-consumerディレクトリに移動して、下記のコマンドを実行してください。(Eclipseから実行する場合は、あらかじめ実装されているConsumerApplicationContractTestを「JUnitテスト」で実行してください。Stub Runner Boot Applicationや実物のプロデューサが起動している場合は停止しておいてください。)

gradlew test

Gradleのテスト結果レポートがdemo-cdctest\cdctest-rest-consumer\build\reports\tests\test\index.htmlに出力されます。コンシューマがContractで定義した値をHTTPリクエストしたか、プロデューサスタブがContractに基づいてHTTPレスポンスした値をコンシューマが正しくハンドリングしたかを検証できます。

Gradleの設定

Spring Cloud Contractを組み込むためのGradleの設定をご紹介します。トレーニングアプリケーションでは、cdctest-rest-api/cdctest-rest-producer/cdctest-rest-consumerの各プロジェクトにGradleの設定「build.gradle」を配置しています。親子関係にはしていません。

Spring Cloud Contractの設定を主に記述するのはcdctest-rest-apiのbuild.gradleです。Contractからコンシューマスタブを自動生成・実行する設定を記述します。cdctest-rest-producerは設定不要です。cdctest-rest-consumerのbuild.gradleにはプロデューサスタブへの依存関係を設定します。

cdctest-rest-apiのGradle設定

まず、cdctest-rest-apiのbuild.gradleの詳細を説明します。

  • demo-cdctest/cdctest-rest-api/build.gradle
buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'org.springframework.cloud:spring-cloud-contract-gradle-plugin:2.2.5.RELEASE'
    }
}

plugins {
    id 'java'
    id 'io.spring.dependency-management' version '1.0.10.RELEASE'
}

apply plugin: 'spring-cloud-contract'
apply plugin: 'maven'
apply plugin: 'eclipse'

group = 'jp.co.ogis_ri.rd.nautible.cdctest'
version = '0.1.0-SNAPSHOT'
sourceCompatibility = '1.8'

jar {
    manifest {
        attributes(
            'Implementation-Title': project.name,
            'Implementation-Version': project.version,
            'Created-By': "Gradle ${gradle.gradleVersion}",
            'Built-By': "${System.properties['user.name']}",
            'Build-Timestamp': new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ").format(new Date()),
            'Build-Jdk': "${System.properties['java.version']} (${System.properties['java.vendor']} ${System.properties['java.vm.version']})",
            'Build-OS': "${System.properties['os.name']} ${System.properties['os.arch']} ${System.properties['os.version']}"
        )
    }
}

tasks.withType(JavaCompile) {
    options.encoding = 'UTF-8'
}

sourceSets.test {
    java.srcDirs = ['src/test/java', 'src/test/gen-java']
    resources.srcDirs = ['src/test/resources', 'src/test/gen-resources']
}

repositories {
    mavenCentral()
    mavenLocal()
}

dependencies {

    testImplementation('org.springframework.boot:spring-boot-starter-test:2.3.7.RELEASE') {
        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }
    testImplementation 'org.springframework.cloud:spring-cloud-starter-contract-verifier:2.2.5.RELEASE'

}

dependencyManagement {
    imports {
        mavenBom 'org.springframework.cloud:spring-cloud-dependencies:Hoxton.SR9'
    }
}

contracts {
    testFramework = 'JUNIT5'
    testMode = 'EXPLICIT'
    generatedTestSourcesDir = project.file('src/test/gen-java')
    generatedTestResourcesDir = project.file('src/test/gen-resources')
    basePackageForTests = 'jp.co.ogis_ri.rd.nautible.cdctest.rest.contracttest'
    packageWithBaseClasses = 'jp.co.ogis_ri.rd.nautible.cdctest.rest.contracttest'
}

test {
    useJUnitPlatform()
    systemProperty 'file.encoding', 'utf-8'
}

ここで読み解くべきポイントは以下の通りです。

  • buildscriptクロージャでspring-cloud-contract-gradle-pluginへの依存関係を設定し、apply plugin: ‘spring-cloud-contract'で有効化する
  • dependenciesクロージャでspring-cloud-starter-contract-verifierを設定する。スコープはテストのみ
  • dependencyManagementクロージャでspring-cloud-dependenciesの依存関係をインポート
  • contractsクロージャで自動生成するコンシューマスタブに関する設定を記述する
    • ここでコンシューマスタブの自動生成出力先を設定した場合、sourceSets.testクロージャで出力先パスをgradleにも設定しておく

主要なcontractsクロージャで設定できる内容は以下の通りです。詳細な設定項目・内容は公式ドキュメントを参照してください。

設定項目 項目概要 解説・備考
testFramework
  • コンシューマスタブのテストフレームワークを指定する
  • 設定可能な値は以下
    • JUNIT5:JUnit5を使用します
    • JUNIT:JUnit4を使用します
    • ・・・
-
testMode
  • コンシューマスタブがプロデューサを呼び出す方式を指定します。HTTPや各種フレームワークのMockを選ぶことができます
  • 設定可能な値は以下
    • EXPLICIT:実際にHTTPで通信して呼び出し
    • MockMvc:Spring MVCのモックとして呼び出し
    • JAXRSCLIENT:JAX-RSのクライアントとサーバとして呼び出し
    • ・・・
汎用性、単体テストとの棲み分け、実稼働に近づける意味でEXPLICITをお勧めします。
実際に通信させるとテスト所要時間に難がある場合や、アドホックな動作確認で使用する場合に各種フレームワークのMockを使用するとよいでしょう。
generatedTestSourcesDir
  • 自動生成されるコンシューマスタブ(テストコード)の出力先パス
Spring Cloud ContractのデフォルトではGradleのビルドディレクトリに出力されます。
トレーニングアプリケーションでは、Eclipseから参照・実行しやすくするため、src/test/gen-javaに変更しています。
basePackageForTests
  • 自動生成されるコンシューマスタブ(テストリソース)のパッケージ
-
packageWithBaseClasses
  • 自動生成されるコンシューマスタブ(テストリソース)の継承元クラスのパッケージ
-

cdctest-rest-consumerのGradle設定

続いて、cdctest-rest-consumerのbuild.gradleの設定です。

  • demo-cdctest/cdctest-rest-consumer/build.gradle
plugins {
    id 'org.springframework.boot' version '2.3.7.RELEASE'
    id 'io.spring.dependency-management' version '1.0.10.RELEASE'
    id 'java'
}

group = 'jp.co.ogis_ri.rd.nautible.cdctest'
version = '0.1.0-SNAPSHOT'
sourceCompatibility = '1.8'

jar {
    manifest {
        attributes(
            'Implementation-Title': project.name,
            'Implementation-Version': project.version,
            'Created-By': "Gradle ${gradle.gradleVersion}",
            'Built-By': "${System.properties['user.name']}",
            'Build-Timestamp': new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ").format(new Date()),
            'Build-Jdk': "${System.properties['java.version']} (${System.properties['java.vendor']} ${System.properties['java.vm.version']})",
            'Build-OS': "${System.properties['os.name']} ${System.properties['os.arch']} ${System.properties['os.version']}"
        )
    }
}

tasks.withType(JavaCompile) {
    options.encoding = 'UTF-8'
}

repositories {
    mavenCentral()
    mavenLocal()
}

dependencies {

    compileOnly 'org.projectlombok:lombok:1.18.16'
    annotationProcessor 'org.projectlombok:lombok:1.18.16'

    implementation 'org.springframework.boot:spring-boot-starter:2.3.7.RELEASE'
    implementation 'org.springframework:spring-web:5.2.12.RELEASE'

    testImplementation('org.springframework.boot:spring-boot-starter-test:2.3.7.RELEASE') {
        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }

    testImplementation 'org.springframework.cloud:spring-cloud-starter-contract-stub-runner:2.2.5.RELEASE'

    testImplementation('jp.co.ogis_ri.rd.nautible.cdctest:cdctest-rest-api:0.1.0-SNAPSHOT:stubs') {
        transitive = false
    }

}

dependencyManagement {
    imports {
        mavenBom 'org.springframework.cloud:spring-cloud-dependencies:Hoxton.SR9'
    }
}

test {
    useJUnitPlatform()
    systemProperty 'file.encoding', 'utf-8'
}

ここで読み解くべきポイントは以下の通りです。

  • dependenciesクロージャで以下の依存関係を設定する
    • プロデューサスタブを実行するspring-cloud-starter-contract-stub-runnerをテストスコープで設定
    • プロデューサスタブとして実行するContractを含むjp.co.ogis_ri.rd.nautible.cdctest:cdctest-rest-apiをテストスコープ/スタブパッケージングで設定
  • dependencyManagementクロージャでspring-cloud-dependenciesの依存関係をインポート

上記はプロデューサスタブをJUnitテストコード内へ埋め込む場合の設定ポイントです。Stub Runner Boot Applicationの実行でプロデューサスタブを起動する場合、上記の設定は不要です。Gradleの外から制御するとよいでしょう。

構成管理・CIのプラクティス

最後に、みなさんのサービスにSpring Cloud Contractを組み込むにあたって、構成管理・CIのプラクティスをご紹介します。ポイントは以下の2つです。

  • Contractから生成される成果物は構成管理の対象から外し、CIでビルド毎に生成する
  • プロデューサからContractを含むAPIを切り出して、独立したプロジェクトとして構成管理する

Contractから生成される成果物は構成管理の対象から外す

自動生成する成果物はCIでビルド毎に生成しなおし、最新のContractの内容を確実に反映するとよいでしょう。最新のContractでコンパイル・テストを自動実行することで、インターフェース不一致・デグレードを検出できます。構成管理対象から外すことで、誤って自動生成された成果物を手作業で修正してしまい逆に生産性を下げるリスクを排除することもできます。

公開したトレーニングアプリケーションにCIサーバの設定は含めていませんが、本記事でご紹介したgradleの各TaskをCIサーバから実行すれば、開発環境同様にコンシューマスタブ・Contractを含むjarを生成できます。

gitで構成管理対象から外す場合、.gitignoreを設定してください。cdctest-rest-api/cdctest-rest-producer/cdctest-rest-consumerの各プロジェクトに設定例を含めています。

Contractを含むAPIのインターフェースを切り出す

Contractを含むAPIのインターフェースはプロデューサから切り出して、独立したプロジェクトとして扱うとよいでしょう。前回説明した通り、Contractはコンシューマ主導で作成する成果物です。Contractが別チーム管理であるプロデューサのプロジェクト内に配置されていると、コンシューマ開発者から更新し難いのは容易に想像できるでしょう。プロデューサとしても、自身の都合でプロジェクト内部構成が変更し難くなります。

そこで、APIを独立プロジェクトとして切り出す手法が効果を発揮します。APIがインターフェースのみ切り出されていれば、コンシューマ/プロデューサ双方が気軽にContractを更新でき、最新の設計内容に追従しやすくなります。

さらに、Contract以外にもSwagger-Specや(プログラミング言語の)インターフェースやモデルクラスなどコンシューマ/プロデューサ共有資源は存在します。これらの格納場所としても適切でしょう。

そして何より、プロデューサの実装とは別に、API自体にバージョンを付与して管理できます。後方互換性の有無などをAPI単位で統一規約(セマンティック・バージョニングが有名です)のもと表現できるとコミュニケーションが楽になります。

その他、Spring Cloud Contractの公式ドキュメントに様々なケースに対応した構成管理とワークフローの例が示されています。興味があれば参照してください。

まとめ

「マイクロサービスアーキテクチャに効く!テスト技法」の第2回目として、Spring Cloud Contractを用いたREST APIのCDCテストを実施しました。

次回も引き続き「実践編」として、メッセージキューを使用したより複雑なインターフェース仕様をSpring Cloud ContractでCDCテストします。ご期待ください。

1: インストールパスは目安です。みなさんの好みで変更しても問題ありません。トレーニングアプリケーション中に絶対パスを使用している箇所はありません

2: Stub Runner Boot ApplicationはSpring Cloud Contract本体と同じサイクルでバージョンアップされており、Mavenセントラルリポジトリで配布されています。Spring Cloud Contract本体と同一バージョンを使用することをお勧めします。本記事では、Stub Runner Boot Application 2.2.5.RELEASEを使用します


変更履歴:

  • 本文中に記載したGitHubのURLを変更しました(2021.6.22)
  • 本文中に記載したGitHubのURLを変更しました(2022.4.21)