daisuzz.log

はじめてのSpring Cloud Contract

はじめに

最近、JSUG勉強会でSpring Cloud Contractを知って面白そうだったので、今回はSpring Cloud Contractを使ってConsumer Driven Contract Testingを実現する方法を紹介します。

Consumer Driven Contract Testingとは

Consumer Driven Contract(以下、CDC)とは、以下の図のようにサービスの提供側(Provider)とサービスの呼び出し側(Consumer)という2つのサービスがあるときに、Consumerが期待したProviderの振る舞い、を定義したものを指します。

f:id:dais39:20201008001430p:plain

Consumer Driven Contract Testingは、Providerを呼び出しときのリクエスト形式やProviderのレスポンス形式を、Consumer側で契約として定義しておき、その契約を使ってProviderの破壊的変更がないかテストを行ったり、契約に基づいて作成されたスタブを使ってConsumerのテストを行うことを指します。

CDC自体の考え方は新しいものではなく、2006年にmartinFowler.comで説明されています。 ここ数年は、MicroServiceにおけるテストの文脈でCDCが注目されていて、OreillyのマイクロサービスアーキテクチャでもCDCが紹介されています。

マイクロサービスでは、各サービスが他のサービスとやりとりを行う処理をテストする場合、

  • 全てのマイクロサービスをデプロイして、E2Eテストを行う

  • テスト対象のサービス以外をモックにして、ユニットテスト結合テストを行う

という2つのやり方があります。

全てのマイクロサービスをデプロイしてE2Eテストを行う場合、より本番環境に近い状態でサービスをテストできる、というメリットがあります。 しかし一方で、一つのサービスをテストするために、他のサービスやデータベースなどをデプロイする必要があったり、テストの実行や実行結果を得られるまでに時間がかかってしまったり、デバッグをするのが難しい、といったデメリットもあります。

また、テスト対象のサービス以外をモックにして、ユニットテスト結合テストを行う場合は、テストの実行や実行結果を得られるまでの時間が短時間である、デプロイを必要としない、といったメリットがある一方で、プロダクションコードに全く関係がないスタプを実装する必要があったり、スタブを使うことによってテストはパスするが、本番環境で失敗するようなケースを検知できない、といったデメリットがあります。

こういったマイクロサービスにおけるテストの課題を解決/軽減するためにConsumer Driven Contract Testingが注目されている、という背景があります。

Spring Cloud Contract

Spring Cloud Contractは、Consumer Driven Contract Testingを実現するために開発されたSpring Cloud内のプロジェクトです。

github.com

元々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のテストを作成する方法を紹介します。

環境

今回は、以下のサンプルアプリケーションを使って説明していきます。 サンプルアプリケーションの環境は以下です。

  • Spring Boot 2.3.2.RELEASE
  • Spring Cloud Contract 2.2.3.RELEASE
  • Java 14
  • Maven 3
  • JUnit5

今回はあらかじめ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をしてもらえるなんて感謝です。

デフォルトでは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-plugincontractsDirectoryディレクトリを指定します。

    <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.xmlspring-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でYAMLJavaの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の場合も、公式ドキュメントに書かれているので気になる方は以下を参照してください。

https://docs.spring.io/spring-cloud-contract/docs/current/reference/html/project-features.html#contract-dsl-multiple

テストの基本クラスを作成

自動生成されたテストクラスが継承する基本クラスを作成します。 今回はテストで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.xmlspring-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.xmlspring-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);
    }
}

@AutoConfigureStubRunneridsでProviderのgroupId, artifactId, version, スタブサーバのport番号を指定します。 上の例では、groupIdがcom.daisuzz, artifactIdがsample-spring-cloud-contract-server, versionがlatest(+は最新のバージョンを表します), port番号が8082のスタブサーバを指定しています。

stubsModeidsで指定したパッケージの取得先を指定しています。今回はローカルリポジトリから取得するので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

Spring Cloud COntract Reference Documentation

マイクロサービスアーキテクチャ