前回はサービス間のインターフェース仕様をテストする「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 | - |
|
Java仮想マシン | JDK8 (Amazon Corretto 8で動作確認済) |
C:\Program Files\Amazon Corretto\jdk1.8.0_275 |
|
IDE | Eclipse Photon 2018 | C:\Eclipse |
|
ワークスペース | - | C:\workspace |
|
なお、開発PCはインターネットに接続できることが前提です。ビルド実行時、インターネットからSpring Cloud ContractやGradleなど各種ライブラリを自動ダウンロードします。
公開しているソースコードはご自身の責任の元でご利用頂くようお願いいたします。CDCテスト / Spring Cloud Contractの個人的学習を目的とするものであり、パッチ適用や脆弱性検査を含むメンテナンスは実施しておりません。本ソースコードに関する問題点及びその問題点が原因で発生した損害等に関して、オージス総研ならびにオブジェクトの広場編集部は一切責任を持ちません。
トレーニングアプリケーションの仕様・構成・ビルド・起動
今回のトレーニングアプリケーションは、以下3つのプロジェクトで構成されています。
プロジェクト | 役割・用途・実装の概要 | 内容物 |
---|---|---|
cdctest-rest-api |
|
|
cdctest-rest-producer |
|
|
cdctest-rest-consumer |
|
|
これらプロジェクトは、以下の仕様・配置で構成されます。
各プロジェクトをビルドして、動作確認を行います。下記手順を実施してください。
cdctest-rest-apiのビルドと実行
- demo-cdctest/cdctest-rest-apiディレクトリに移動
以下コマンドを実行してビルド
gradlew build -x test
以下コマンドを実行してMavenローカルリポジトリへインストール
gradlew install
cdctest-rest-producerのビルドと実行
- demo-cdctest/cdctest-rest-producerディレクトリに移動
以下コマンドを実行してビルド
gradlew build -x test
以下コマンドを実行してプロデューサ起動
java -jar .\build\libs\cdctest-rest-producer-0.1.0-SNAPSHOT.jar
cdctest-rest-consumerのビルドと実行
- demo-cdctest/cdctest-rest-consumerディレクトリに移動
以下コマンドを実行してビルド
gradlew build -x test
動作確認
- demo-cdctest/cdctest-rest-consumerディレクトリに移動
以下コマンドを実行してコンシューマを実行
java -jar .\build\libs\cdctest-rest-consumer-0.1.0-SNAPSHOT.jar
標準出力に下記が出力されてコンシューマ→プロデューサへ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の数が増えると、テストの意図がわかりにくくなる | |
name | 文字列 |
|
日本語使用可能 | |
ignored | 真偽 |
|
- | |
request | url | 文字列 |
|
ホスト名・ポート番号は環境毎に異なるため、コンシューマスタブ・プロデューサスタブで設定する |
queryParameters | ハッシュの配列 |
|
- | |
method | 文字列 |
|
- | |
headers | ハッシュの配列 |
|
- | |
body | オブジェクト |
|
YAMLのオブジェクトで定義した内容が、コンシューマスタブ・プロデューサスタブではJSONに展開される | |
matchers | オブジェクト |
|
URL、リクエストヘッダ、リクエストボディ等でオブジェクト構造(値範囲定義の仕方)が異なる。詳細は公式ドキュメント参照 | |
response | status | 整数 |
|
- |
headers | ハッシュの配列 |
|
- | |
body | オブジェクト |
|
YAMLのオブジェクトで定義した内容が、コンシューマスタブ・プロデューサスタブではJSONに展開される | |
matchers | オブジェクト |
|
レスポンスヘッダ、レスポンスボディ等でオブジェクト構造(値範囲定義の仕方)が異なる。詳細は公式ドキュメント参照 |
また、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 Boot・RestAssured(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テストコードがSpring Bootに依存します。 プロデューサスタブ(JUnitテストコード)とコンシューマを同一JVMで実行する場合、依存ライブラリ構成によっては正常動作しない可能性があります。 |
Stub Runner Boot Application実行 |
|
|
コンシューマとは完全に別のプロセスで動作するため、様々なアプリケーションで確実に適用できます。 |
なお、さきほど起動した(実物の)プロデューサは停止しておいてください。起動したままだと、ポート番号が重複してスタブが起動しません。
プロデューサスタブを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 |
|
ids |
|
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 |
|
- |
testMode |
|
汎用性、単体テストとの棲み分け、実稼働に近づける意味で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)