はじめに
最近、JSUG勉強会でSpring Cloud Contractを知って面白そうだったので、今回はSpring Cloud Contractを使ってConsumer Driven Contract Testingを実現する方法を紹介します。
Consumer Driven Contract Testingとは
Consumer Driven Contract(以下、CDC)とは、以下の図のようにサービスの提供側(Provider)とサービスの呼び出し側(Consumer)という2つのサービスがあるときに、Consumerが期待したProviderの振る舞い、を定義したものを指します。
Consumer Driven Contract Testingは、Providerを呼び出しときのリクエスト形式やProviderのレスポンス形式を、Consumer側で契約として定義しておき、その契約を使ってProviderの破壊的変更がないかテストを行ったり、契約に基づいて作成されたスタブを使ってConsumerのテストを行うことを指します。
CDC自体の考え方は新しいものではなく、2006年にmartinFowler.comで説明されています。 ここ数年は、MicroServiceにおけるテストの文脈でCDCが注目されていて、OreillyのマイクロサービスアーキテクチャでもCDCが紹介されています。
マイクロサービスでは、各サービスが他のサービスとやりとりを行う処理をテストする場合、
という2つのやり方があります。
全てのマイクロサービスをデプロイしてE2Eテストを行う場合、より本番環境に近い状態でサービスをテストできる、というメリットがあります。 しかし一方で、一つのサービスをテストするために、他のサービスやデータベースなどをデプロイする必要があったり、テストの実行や実行結果を得られるまでに時間がかかってしまったり、デバッグをするのが難しい、といったデメリットもあります。
また、テスト対象のサービス以外をモックにして、ユニットテストや結合テストを行う場合は、テストの実行や実行結果を得られるまでの時間が短時間である、デプロイを必要としない、といったメリットがある一方で、プロダクションコードに全く関係がないスタプを実装する必要があったり、スタブを使うことによってテストはパスするが、本番環境で失敗するようなケースを検知できない、といったデメリットがあります。
こういったマイクロサービスにおけるテストの課題を解決/軽減するためにConsumer Driven Contract Testingが注目されている、という背景があります。
Spring Cloud Contract
Spring Cloud Contractは、Consumer Driven Contract Testingを実現するために開発されたSpring Cloud内のプロジェクトです。
元々JavaでConsumer Driven Contract Testingを実現するために開発されていたAccurestというOSSが、SpringのプロジェクトとなったことでSpring Cloud Contractという名前になりました。
Spring Cloud Contractは、HTTPやメッセージングのスタブが、サーバサイドの実際の挙動を正しく再現して動作することを保証したり、サーバサイドで利用されるテストコードのポイラープレートを自動で生成することなどを目的として開発されています。
Spring Cloud Contractを使うことで、Groovy, Java, Kotlin, YAMLのいずれかで書かれたCDCを基にProviderのテストコードや、Consumerが依存するProviderのスタブサーバを自動生成することができます。
Spring Cloud Contractを使ってみる
ここからは、実際にSpring Cloud Contractを使ってProviderのテストの自動生成と, ProviderのスタブをつかったConsumerのテストを作成する方法を紹介します。
環境
今回は、以下のサンプルアプリケーションを使って説明していきます。 サンプルアプリケーションの環境は以下です。
今回はあらかじめProvider(port:8082)とConsumer(port:8081)を用意しておき、そこにSpring Cloud Contractを導入していきます。
Provider
Providerはユーザ情報を返すREST APIです。
package com.daisuzz.samplespringcloudcontractserver; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class UserController { @GetMapping("user") private User getUser() { return new User("Yamada", "Taro", 20); } }
package com.daisuzz.samplespringcloudcontractserver; public class User { String lastName; String firstName; Integer age; User(String lastName, String firstName, Integer age) { this.lastName = lastName; this.firstName = firstName; this.age = age; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } }
Consumer
Consumerは、UserClient経由でProviderからユーザ情報を取得し、そのユーザ情報を返すREST APIです。
package com.daisuzz.samplespringcloudcontractclient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class SampleController { private final UserClient client; public SampleController(UserClient client) { this.client = client; } @GetMapping("sample") public User sample() { return client.getUser(); } }
package com.daisuzz.samplespringcloudcontractclient; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; import org.springframework.web.client.RestTemplate; @Component public class UserClient { private final RestTemplate restTemplate; public UserClient(RestTemplate restTemplate) { this.restTemplate = restTemplate; } public User getUser() { ResponseEntity<User> response = restTemplate.getForEntity("http://localhost:8082/user", User.class); return response.getBody(); } }
ProviderにSpring Cloud Contractを導入する
pom.xmlの設定
ProviderにSpring Cloud Contractを導入して、CDCに基づいてテストを自動生成できるようにしていきます。
まずは、pom.xmlのdependenciesにspring-cloud-starter-contract-verifier
、pluginにspring-cloud-contract-maven-plugin
をそれぞれ追加します。
<dependencies> // 中略 <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-contract-verifier</artifactId> <version>2.2.3.RELEASE</version> </dependency> </dependencies> <build> <plugins> // 中略 <plugin> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-contract-maven-plugin</artifactId> <version>2.2.3.RELEASE</version> <extensions>true</extensions> <configuration> <baseClassForTests>com.daisuzz.samplespringcloudcontractserver.SampleUserTests</baseClassForTests> <testFramework>JUNIT5</testFramework> </configuration> </plugin> </plugins> </build>
spring-cloud-starter-contract-verifier
は、YAML, Groovy, Java, Kotlinで書かれたCDCから、WireMockを使ったテストを自動生成してくれるライブラリ、
spring-cloud-contract-maven-plugin
は、Mavenのプラグインです。
generateTestsゴールでテストを自動生成してくれるため、mvn clean spring-cloud-contract:generateTests
を実行することで、テストを自動生成することができます。
spring-cloud-contract-maven-plugin
のconfigurationの中身のtestFramework
は、今回JUnit5を利用するためJUNIT5
指定しています。デフォルトはJUnit4を利用します。
baseClassForTests
については後ほど説明します。
CDCの作成
Spring Cloud ContractではYAML, Groovy, Java, KotlinでCDCを定義することができます。 TwitterでこれをツイートしたらSpring Cloud ContractのAuthorがKotlinでの利用方法を教えてくれました。日本語のTweetなのにreplyをしてもらえるなんて感謝です。
Yes, you can :) If you add `spring-cloud-contract-kotlin-spec` to your classpath we will analyze also kotlin files for contracts. Java, Yaml and Groovy are scanned out of the box.
— Marcin Grzejszczak (@MGrzejszczak) 2020年8月3日
デフォルトではsrc/test/resources/contracts
配下にCDCを定義します。
YAMLの場合
YAMLを使ってCDCを書いた場合は以下にようになります。
description: Sample contract name: userContract request: url: /user method: GET response: status: 200 headers: Content-Type: application/json body: lastName: Yamada firstName: Taro age: 20
descriptionには、CDCを説明するテキストを記述します。
nameには、自動生成された際に自動で作成されるテストメソッド名を記述します。例えばuserContract
と記述すると、自動生成されたテストメソッド名はvalidate_userContract
になります。
requestには、Consumerが送るリクエストを記述します。この例では/user
というエンドポイントにGETリクエストを送ることを記述しています。
responseには、Consumerが期待するProducerからのレスポンスを記述します。この例ではステータスコード200、レスポンスヘッダとしてContent-Type: application/json
、レスポンスボディとしてユーザ情報のサンプルを返すことを記述しています。
他にもたくさん設定項目があるので、気になる人は公式ドキュメントを参照してください。
Javaの場合
Javaで上のYAMLと同じ内容のCDCを書いたものが以下です。Supplier<Collection<Contract>>
を実装したクラスを書いてCDCを表現します。
package contracts; import org.springframework.cloud.contract.spec.Contract; import org.springframework.cloud.contract.verifier.util.ContractVerifierUtil; import java.util.Collection; import java.util.Collections; import java.util.function.Supplier; public class UserContract implements Supplier<Collection<Contract>> { @Override public Collection<Contract> get() { return Collections.singletonList(Contract.make(c -> { c.description("Sample contract"); c.name("userContract"); c.request(r -> { r.url("/user"); r.method(r.GET()); }); c.response(r -> { r.status(200); r.headers(h -> { h.contentType("application/json"); }); r.body(ContractVerifierUtil.map() .entry("lastName", "Yamada") .entry("firstName", "Taro") .entry("age", 20)); }); })); } }
Javaで書いたCDCは、src/test/java
もしくはsrc/test/java/contracts
に配置することができます。
その場合はspring-cloud-contract-maven-plugin
のcontractsDirectory
でディレクトリを指定します。
<build> <plugin> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-contract-maven-plugin</artifactId> <version>2.2.3.RELEASE</version> <extensions>true</extensions> <configuration> <contractsDirectory>src/test/java/contracts</contractsDirectory> <baseClassForTests>com.daisuzz.samplespringcloudcontractserver.SampleUserTests</baseClassForTests> <testFramework>JUNIT5</testFramework> </configuration> </plugin> </build>
Kotlinの場合
KotlinでCDCを定義する場合、pom.xmlにspring-cloud-contract-spec-kotlin
を追加します。
<dependencies> // 中略 <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-contract-spec-kotlin</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugin> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-contract-maven-plugin</artifactId> <version>2.2.3.RELEASE</version> <extensions>true</extensions> <configuration> <contractsDirectory>src/test/kotlin/contracts</contractsDirectory> <baseClassForTests>com.daisuzz.samplespringcloudcontractserver.SampleUserTests</baseClassForTests> <testFramework>JUNIT5</testFramework> </configuration> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-contract-spec-kotlin</artifactId> <version>2.2.3.RELEASE</version> </dependency> </dependencies> </plugin> </build>
Kotlinでは.kts
ファイルにCDCを定義します。場所はデフォルトではsrc/test/resources/contracts
配下ですが、contractsDirectory
で任意の場所を指定することができます。
今回は、src/test/kotlin/contracts
を指定しました。
KotlinでYAMLやJavaのCDCと同じ内容を書いたものが以下です。
import org.springframework.cloud.contract.spec.ContractDsl.Companion.contract import org.springframework.cloud.contract.spec.withQueryParameters contract { name = "Sample contract" description = "userContract" request { url = url("/user") method = GET } response { status = OK headers { header("Content-Type", "application/json") } body = body(mapOf( "lastName" to "Yamada", "firstName" to "Taro", "age" to 20) ) } }
Groovyの場合
Groovyの例は割愛します。 詳しくは公式ドキュメントを参照してください。 https://cloud.spring.io/spring-cloud-contract/reference/html/project-features.html#contract-groovy
複数のContractを1つのファイルに書きたい場合
複数のContractを1つのファイルに書くこともできます。
YAMLの場合は、以下のように---
で区切ることで複数のContractを書くことができます。
description: Sample contract name: get users request: url: /users/1 method: GET response: status: 200 headers: Content-Type: application/json body: id: 1 lastName: Yamada firstName: Taro age: 20 --- name: get user request: url: /users method: GET response: status: 200 headers: Content-Type: application/json body: - id: 1 lastName: Yamada firstName: Taro age: 20 - id: 2 lastName: Suzuki firstName: Ichiro age: 30
JavaやKotlinなど他のDSLの場合も、公式ドキュメントに書かれているので気になる方は以下を参照してください。
テストの基本クラスを作成
自動生成されたテストクラスが継承する基本クラスを作成します。 今回はテストでSpringのApplicationContextを利用したいので、以下の基本クラスを作成しました。
package com.daisuzz.samplespringcloudcontractserver; import io.restassured.module.mockmvc.RestAssuredMockMvc; import org.junit.jupiter.api.BeforeEach; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.web.context.WebApplicationContext; @SpringBootTest class SampleUserTests { @Autowired private WebApplicationContext context; @BeforeEach public void setUp() { RestAssuredMockMvc.webAppContextSetup(context); } }
ここで作成したクラスのFQCNをpom.xmlのspring-cloud-contract-maven-plugin
のconfigurationのbaseClassForTests
として指定することで、このクラスを継承したテストクラスが自動生成されます。
<build> <plugin> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-contract-maven-plugin</artifactId> <version>2.2.3.RELEASE</version> <extensions>true</extensions> <configuration> <baseClassForTests>com.daisuzz.samplespringcloudcontractserver.SampleUserTests</baseClassForTests> <testFramework>JUNIT5</testFramework> </configuration> </plugin> </build>
テストを自動生成
mvn clean spring-cloud-contract:generateTests
を実行すると、target/generated-test-sources/contracts/
配下にCDCを基にしたテストが自動生成されます。
package com.daisuzz.samplespringcloudcontractserver; import com.daisuzz.samplespringcloudcontractserver.SampleUserTests; import com.jayway.jsonpath.DocumentContext; import com.jayway.jsonpath.JsonPath; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import io.restassured.module.mockmvc.specification.MockMvcRequestSpecification; import io.restassured.response.ResponseOptions; import static org.springframework.cloud.contract.verifier.assertion.SpringCloudContractAssertions.assertThat; import static org.springframework.cloud.contract.verifier.util.ContractVerifierUtil.*; import static com.toomuchcoding.jsonassert.JsonAssertion.assertThatJson; import static io.restassured.module.mockmvc.RestAssuredMockMvc.*; @SuppressWarnings("rawtypes") public class ContractVerifierTest extends SampleUserTests { @Test public void validate_userContract() throws Exception { // given: MockMvcRequestSpecification request = given(); // when: ResponseOptions response = given().spec(request) .get("/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("['lastName']").isEqualTo("Yamada"); assertThatJson(parsedJson).field("['firstName']").isEqualTo("Taro"); assertThatJson(parsedJson).field("['age']").isEqualTo(20); } }
この自動生成されたテストでは、CDCで定義したURLに対してリクエストを送り、CDCで定義したレスポンスが返ってくるかをテストしています。
このテストをProviderの変更時に必ず実行することで、ProviderにCDCに定義されていない破壊的変更を検知することができます。
実際にmvn test
を実行すると、テストが通ることが確認できます。
$ mvn test // 中略 [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 8.581 s - in com.daisuzz.samplespringcloudcontractserver.ContractVerifierTest 2020-08-04 22:00:15.845 INFO 4173 --- [extShutdownHook] o.s.s.concurrent.ThreadPoolTaskExecutor : Shutting down ExecutorService 'applicationTaskExecutor' [INFO] [INFO] Results: [INFO] [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 [INFO] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 20.677 s [INFO] Finished at: 2020-08-04T22:00:16+09:00 [INFO] ------------------------------------------------------------------------
ConsumerにSpring Cloud Contractを導入する
ここまで、ProviderのテストをCDCから自動生成する流れを説明しました。
ここからはConsumerがProviderに依存したテストを行う際に、利用するProviderのスタブをCDCから自動生成する流れを説明します。
pom.xmlの設定
まずは、Consumerのpom.xmlにspring-cloud-starter-contract-stub-runner
を追加します。
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-contract-stub-runner</artifactId> <version>2.2.3.RELEASE</version> </dependency> </dependencies>
spring-cloud-starter-contract-stub-runner
を追加することで、あらかじめProviderが生成したstub用のjarファイルを読み込んで、テスト実行時にスタンドアロンなスタブサーバを利用することができます。
Providerを利用したConsumerのテストを作成
次に、Providerを利用したConsumerのテストを書いていきます。
package com.daisuzz.samplespringcloudcontractclient; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.cloud.contract.stubrunner.spring.AutoConfigureStubRunner; import org.springframework.cloud.contract.stubrunner.spring.StubRunnerProperties; import org.springframework.test.context.junit.jupiter.SpringExtension; import static org.junit.jupiter.api.Assertions.assertEquals; @ExtendWith(SpringExtension.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) @AutoConfigureStubRunner(ids = {"com.daisuzz:sample-spring-cloud-contract-server:+:stubs:8082"}, stubsMode = StubRunnerProperties.StubsMode.LOCAL) class UserClientTests { @Autowired UserClient userClient; @Test public void validateUserStub() { User user = userClient.getUser(); assertEquals(user.lastName, "Yamada"); assertEquals(user.firstName, "Taro"); assertEquals(user.age, 20); } }
@AutoConfigureStubRunner
のids
でProviderのgroupId, artifactId, version, スタブサーバのport番号を指定します。
上の例では、groupIdがcom.daisuzz
, artifactIdがsample-spring-cloud-contract-server
, versionがlatest
(+は最新のバージョンを表します), port番号が8082のスタブサーバを指定しています。
stubsMode
はids
で指定したパッケージの取得先を指定しています。今回はローカルリポジトリから取得するのでStubRunnerProperties.StubMode.LOCAL
を指定しています。
artifactoryなどのリモートリポジトリからパッケージを取得したい場合は、StubRunnerProperties.StubMode.REMOTE
, classpathから取得したい場合はStubRunnerProperties.StubMode.CLASSPATH
を指定します。
テストの実行
mvn test
でテストを実行してみます。
途中のログをみてみると、以下でstub用ののパッケージを解決して、スタブサーバを8082ポートで起動しているのが確認できます。
2020-08-04 22:18:21.352 INFO 4286 --- [ main] o.s.c.c.s.AetherStubDownloaderBuilder : Will download stubs and contracts via Aether 2020-08-04 22:18:21.359 INFO 4286 --- [ main] o.s.c.c.stubrunner.AetherStubDownloader : Remote repos not passed but the switch to work offline was set. Stubs will be used from your local Maven repository. 2020-08-04 22:18:21.491 INFO 4286 --- [ main] o.s.c.c.stubrunner.AetherStubDownloader : Desired version is [+] - will try to resolve the latest version 2020-08-04 22:18:21.507 INFO 4286 --- [ main] o.s.c.c.stubrunner.AetherStubDownloader : Resolved version is [0.0.1-SNAPSHOT] 2020-08-04 22:18:21.514 INFO 4286 --- [ main] o.s.c.c.stubrunner.AetherStubDownloader : Resolved artifact [com.daisuzz:sample-spring-cloud-contract-server:jar:stubs:0.0.1-SNAPSHOT] to /Users/daisuzz/.m2/repository/com/daisuzz/sample-spring-cloud-contract-server/0.0.1-SNAPSHOT/sample-spring-cloud-contract-server-0.0.1-SNAPSHOT-stubs.jar 2020-08-04 22:18:21.515 INFO 4286 --- [ main] o.s.c.c.stubrunner.AetherStubDownloader : Unpacking stub from JAR [URI: file:/Users/daisuzz/.m2/repository/com/daisuzz/sample-spring-cloud-contract-server/0.0.1-SNAPSHOT/sample-spring-cloud-contract-server-0.0.1-SNAPSHOT-stubs.jar] 2020-08-04 22:18:21.520 INFO 4286 --- [ main] o.s.c.c.stubrunner.AetherStubDownloader : Unpacked file to [/var/folders/hk/n3hhgwbs5h3ds7z_vs9rstvr0000gn/T/contracts-1596547101515-0] 2020-08-04 22:18:22.626 INFO 4286 --- [ main] wiremock.org.eclipse.jetty.util.log : Logging initialized @7943ms to wiremock.org.eclipse.jetty.util.log.Slf4jLog 2020-08-04 22:18:22.763 INFO 4286 --- [ main] w.org.eclipse.jetty.server.Server : jetty-9.4.20.v20190813; built: 2019-08-13T21:28:18.144Z; git: 84700530e645e812b336747464d6fbbf370c9a20; jvm 14.0.1+7 2020-08-04 22:18:22.785 INFO 4286 --- [ main] w.o.e.j.server.handler.ContextHandler : Started w.o.e.j.s.ServletContextHandler@f03ee8f{/__admin,null,AVAILABLE} 2020-08-04 22:18:22.787 INFO 4286 --- [ main] w.o.e.j.server.handler.ContextHandler : Started w.o.e.j.s.ServletContextHandler@1c68d0db{/,null,AVAILABLE} 2020-08-04 22:18:22.820 INFO 4286 --- [ main] w.o.e.jetty.server.AbstractConnector : Started NetworkTrafficServerConnector@214b342f{HTTP/1.1,[http/1.1]}{0.0.0.0:8082} 2020-08-04 22:18:22.821 INFO 4286 --- [ main] w.org.eclipse.jetty.server.Server : Started @8142ms 2020-08-04 22:18:22.821 INFO 4286 --- [ main] o.s.c.contract.stubrunner.StubServer : Started stub server for project [com.daisuzz:sample-spring-cloud-contract-server:0.0.1-SNAPSHOT:stubs] on port 8082
その後のログをみると、/user
にGETリクエストを投げているログが確認できます。
Connection: [keep-alive] User-Agent: [Apache-HttpClient/4.5.6 (Java/14.0.1)] Host: [localhost:8082] Content-Length: [395] Content-Type: [text/plain; charset=UTF-8] { "id" : "d3e01a60-312a-4379-945e-f97dbe1227db", "request" : { "url" : "/user", "method" : "GET" }, "response" : { "status" : 200, "body" : "{\"lastName\":\"Yamada\",\"firstName\":\"Taro\",\"age\":20}", "headers" : { "Content-Type" : "application/json" }, "transformers" : [ "response-template" ] }, "uuid" : "d3e01a60-312a-4379-945e-f97dbe1227db" } 2020-08-04 22:18:23.321 INFO 4286 --- [ main] o.s.c.c.stubrunner.StubRunnerExecutor : All stubs are now running RunningStubs [namesAndPorts={com.daisuzz:sample-spring-cloud-contract-server:0.0.1-SNAPSHOT:stubs=8082}] 2020-08-04 22:18:23.326 INFO 4286 --- [ main] trationDelegate$BeanPostProcessorChecker : Bean 'batchStubRunner' of type [org.springframework.cloud.contract.stubrunner.BatchStubRunner] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying) 2020-08-04 22:18:23.643 INFO 4286 --- [ main] c.d.s.UserClientTests : Started UserClientTests in 8.149 seconds (JVM running for 8.965) 2020-08-04 22:18:24.027 INFO 4286 --- [tp1342373353-22] w.o.e.j.s.handler.ContextHandler.ROOT : RequestHandlerClass from context returned com.github.tomakehurst.wiremock.http.StubRequestHandler. Normalized mapped under returned 'null' 2020-08-04 22:18:24.129 INFO 4286 --- [tp1342373353-22] WireMock : Request received: 127.0.0.1 - GET /user Accept: [application/json, application/*+json] Connection: [keep-alive] User-Agent: [Apache-HttpClient/4.5.12 (Java/14.0.1)] Host: [localhost:8082] Accept-Encoding: [gzip,deflate] Matched response definition: { "status" : 200, "body" : "{\"lastName\":\"Yamada\",\"firstName\":\"Taro\",\"age\":20}", "headers" : { "Content-Type" : "application/json" }, "transformers" : [ "response-template" ] } Response: HTTP/1.1 200 Content-Type: [application/json] Matched-Stub-Id: [d3e01a60-312a-4379-945e-f97dbe1227db] 2020-08-04 22:18:24.172 WARN 4286 --- [ main] .StubRunnerWireMockTestExecutionListener : You've used fixed ports for WireMock setup - will mark context as dirty. Please use random ports, as much as possible. Your tests will be faster and more reliable and this warning will go away 2020-08-04 22:18:24.181 INFO 4286 --- [ main] w.o.e.jetty.server.AbstractConnector : Stopped NetworkTrafficServerConnector@214b342f{HTTP/1.1,[http/1.1]}{0.0.0.0:8082} 2020-08-04 22:18:24.183 INFO 4286 --- [ main] w.o.e.j.server.handler.ContextHandler : Stopped w.o.e.j.s.ServletContextHandler@1c68d0db{/,null,UNAVAILABLE} 2020-08-04 22:18:24.183 INFO 4286 --- [ main] w.o.e.j.server.handler.ContextHandler : Stopped w.o.e.j.s.ServletContextHandler@f03ee8f{/__admin,null,UNAVAILABLE} [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 9.074 s - in com.daisuzz.samplespringcloudcontractclient.UserClientTests [INFO] [INFO] Results: [INFO] [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 [INFO] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 11.317 s [INFO] Finished at: 2020-08-04T22:18:24+09:00 [INFO] ------------------------------------------------------------------------
Providerのスタブサーバを自動で生成/起動してConsumerのテストを行うことができました。
まとめ
今回は、Spring Cloud Contractを使って、CDCを基にテストやスタブを自動生成する方法を紹介しました。
Spring Cloud Contractを使えば、簡単にテストコードの自動生成やスタブの作成ができるので、使い方によってはとても便利なライブラリになるのではと感じました。 一方で実際にこれをシステムに取り入れるとなると、CDCのメンテナンスをどこのチームでやるかとか、CDC自体の記述量が多くなる、などが課題になると思います。
SwaggerやOpenAPI, Protocol Buffersなどスキーマに基づいてコードやドキュメントを生成するライブラリもありますが、スタブやテストコードを簡単に自動生成できるライブラリは個人的にあまり見ないので、Spring Cloud Contractは一つの選択肢として覚えておこうと思います。
参考資料
Consumer-Driven Contracts: A Service Evolution Pattern