daisuzz.log

daisuzz.log

ソフトウェアエンジニアをしています。プログラミングに関することや読んだ本について書いているブログです。

はじめてのSpring Cloud Contract

はじめに

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

Consumer Driven Contractとは

Consumer Driven Contract(以下、CDC)とは、サービスの利用側(Consumer)側が要求するサービスの提供側(Provider)との契約(Contract)を指します。 Providerの破壊的変更をCDCに基づいて作成されたテストで検知したり、Providerに依存したConsumerのテストをCDCに基づいて作成されたスタブによって、テストを扱いやすいものにすることができます。

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

CDCについては、上記の資料の他に、Spring Cloud Contractの公式ページが参考になります。

Spring Cloud Contract

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

github.com

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を利用するためJUNT5指定しています。デフォルトはJUnit4を利用します。 baseClassForTestsについては後ほど説明します。

CDCの作成

Spring Cloud ContractではYAML, Groovy, Java, KotlinでCDCを定義することができます。 TwitterでこれをツイートしたらSpring Cloud ContractのAuthorがKotlinでの利用方法を教えてくれました。感謝。

デフォルトでは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

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

自動生成されたテストクラスが継承する基本クラスを作成します。 今回はテストでSpringのApplicationContextを利用したいので、以下の基本クラスを作成しました。

package com.daisuzz.samplespringcloudcontractserver;

import io.restassured.module.mockmvc.RestAssuredMockMvc;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.web.context.WebApplicationContext;

@ExtendWith(SpringExtension.class)
@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を基にテストやスタブを自動生成する方法を紹介しました。 そもそもCDCという考え方を知らなかったので、ConsumerがProviderのふるまいを定義して、それに基づいてProviderのテストを行うという考え方が新鮮でした。 実際にこれをシステムに取り入れるとなると、CDCのメンテナンスをどこのチームでやるかとか、CDC自体の記述料が多くなるので管理方法が、課題になるかと思いますが、Spring Cloud Contractを使えば、簡単にテストコードの自動生成やスタブの作成ができるので、そこはとても便利なライブラリだなと感じました。 SwaggerやOpenAPI, Protocol Buffersなどスキーマに基づいてコードやドキュメントを生成するライブラリもありますが、スタブやテストコードを簡単に自動生成できるライブラリは多くないと思うので、Spring Cloud Contractは一つの選択肢として覚えておこうと思います。

参考資料

Consumer-Driven Contracts: A Service Evolution Pattern

Spring Cloud COntract Reference Documentation

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