初めての Spring Cloud 契約ベースのアプリケーションの開発

この短いツアーでは、Spring Cloud Contract を使用して説明します。これは次のトピックで構成されます。

ここでさらに短いツアーを見つけることができます。

この例では、Stub Storage は Nexus/Artifactory です。

次の UML 図は、Spring Cloud Contract の各部分の関連を示しています。

getting-started-three-second

プロデューサー側

Spring Cloud Contract の使用を開始するには、次の例に示すように、Spring Cloud Contract Verifier の依存関係とプラグインをビルドファイルに追加します。

<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-contract-verifier</artifactId>
	<scope>test</scope>
</dependency>

次のリストは、プラグインを追加する方法を示しています。プラグインは、ファイルの build/plugins 部分に配置する必要があります。

<plugin>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-contract-maven-plugin</artifactId>
	<version>${spring-cloud-contract.version}</version>
	<extensions>true</extensions>
</plugin>

開始する最も簡単な方法は、Spring Initializr に移動し、"Web" と "Contract Verifier" を依存関係として追加することです。これにより、前述の依存関係と必要なその他すべてが pom.xml ファイルに取り込まれます (基本テストクラスの設定を除き、これについてはこのセクションで後ほど説明します)。次の図は、Spring Initializr で使用する設定を示しています。

Spring Initializr with Web and Contract Verifier

Groovy DSL または YAML で表現された REST/ メッセージング 契約を含むファイルを、contractsDslDir プロパティで設定された契約 ディレクトリに追加できるようになりました。デフォルトでは、$rootDir/src/test/resources/contracts です。ファイル名は関係ないことに注意してください。任意の命名スキームを使用して、このディレクトリ内で契約を整理できます。

HTTP スタブの場合、契約は、特定のリクエストに対してどのような種類のレスポンスを返すかを定義します (HTTP メソッド、URL、ヘッダー、ステータスコードなどを考慮して)。次の例は、Groovy と YAML の両方の HTTP スタブ 契約を示しています。

  • groovy

  • ヤムル

org.springframework.cloud.contract.spec.Contract.make {
	request {
		method 'PUT'
		url '/fraudcheck'
		body([
			   "client.id": $(regex('[0-9]{10}')),
			   loanAmount: 99999
		])
		headers {
			contentType('application/json')
		}
	}
	response {
		status OK()
		body([
			   fraudCheckStatus: "FRAUD",
			   "rejection.reason": "Amount too high"
		])
		headers {
			contentType('application/json')
		}
	}
}
request:
  method: PUT
  url: /fraudcheck
  body:
    "client.id": 1234567890
    loanAmount: 99999
  headers:
    Content-Type: application/json
  matchers:
    body:
      - path: $.['client.id']
        type: by_regex
        value: "[0-9]{10}"
response:
  status: 200
  body:
    fraudCheckStatus: "FRAUD"
    "rejection.reason": "Amount too high"
  headers:
    Content-Type: application/json;charset=UTF-8

メッセージングを使用する必要がある場合は、次のように定義できます。

  • 入力メッセージと出力メッセージ (送信元、メッセージ本文、ヘッダーを考慮)。

  • メッセージの受信後に呼び出されるメソッド。

  • 呼び出されたときにメッセージをトリガーするメソッド。

次の例は、Camel メッセージング 契約を示しています。

  • groovy

  • ヤムル

def contractDsl = Contract.make {
	name "foo"
	label 'some_label'
	input {
		triggeredBy('bookReturnedTriggered()')
	}
	outputMessage {
		sentTo('activemq:output')
		body('''{ "bookName" : "foo" }''')
		headers {
			header('BOOK-NAME', 'foo')
			messagingContentType(applicationJson())
		}
	}
}
label: some_label
input:
  triggeredBy: bookReturnedTriggered
outputMessage:
  sentTo: activemq:output
  body:
    bookName: foo
  headers:
    BOOK-NAME: foo
    contentType: application/json

./mvnw clean install を実行すると、追加された契約へのアプリケーションの準拠を検証するテストが自動的に生成されます。デフォルトでは、生成されたテストは org.springframework.cloud.contract.verifier.tests. にあります。

生成されるテストは、プラグインで設定したフレームワークとテストの種類に応じて異なる場合があります。

次のリストでは、次のものが見つかります。

  • MockMvc の HTTP 契約のデフォルトのテストモード

  • JAXRS テストモードの JAX-RS クライアント

  • WEBTESTCLIENT テストモードで設定された WebTestClient ベースのテスト (これは、リアクティブな Web-Flux ベースのアプリケーションを使用する場合に特に推奨されます)

これらのテストフレームワークのうち 1 つだけが必要です。MockMvc がデフォルトです。他のフレームワークのいずれかを使用するには、そのライブラリをクラスパスに追加します。

次のリストは、すべてのフレームワークのサンプルを示しています。

  • モック MVC

  • ジャックス

  • Web テストクライアント

@Test
public void validate_shouldMarkClientAsFraud() throws Exception {
    // given:
        MockMvcRequestSpecification request = given()
                .header("Content-Type", "application/vnd.fraud.v1+json")
                .body("{\"client.id\":\"1234567890\",\"loanAmount\":99999}");

    // when:
        ResponseOptions response = given().spec(request)
                .put("/fraudcheck");

    // then:
        assertThat(response.statusCode()).isEqualTo(200);
        assertThat(response.header("Content-Type")).matches("application/vnd.fraud.v1.json.*");
    // and:
        DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
        assertThatJson(parsedJson).field("['fraudCheckStatus']").matches("[A-Z]{5}");
        assertThatJson(parsedJson).field("['rejection.reason']").isEqualTo("Amount too high");
}
public class FooTest {
  WebTarget webTarget;

  @Test
  public void validate_() throws Exception {

    // when:
      Response response = webTarget
              .path("/users")
              .queryParam("limit", "10")
              .queryParam("offset", "20")
              .queryParam("filter", "email")
              .queryParam("sort", "name")
              .queryParam("search", "55")
              .queryParam("age", "99")
              .queryParam("name", "Denis.Stepanov")
              .queryParam("email", "[email protected] (英語)  ")
              .request()
              .build("GET")
              .invoke();
      String responseAsString = response.readEntity(String.class);

    // then:
      assertThat(response.getStatus()).isEqualTo(200);

    // and:
      DocumentContext parsedJson = JsonPath.parse(responseAsString);
      assertThatJson(parsedJson).field("['property1']").isEqualTo("a");
  }

}
@Test
	public void validate_shouldRejectABeerIfTooYoung() throws Exception {
		// given:
			WebTestClientRequestSpecification request = given()
					.header("Content-Type", "application/json")
					.body("{\"age\":10}");

		// when:
			WebTestClientResponse response = given().spec(request)
					.post("/check");

		// then:
			assertThat(response.statusCode()).isEqualTo(200);
			assertThat(response.header("Content-Type")).matches("application/json.*");
		// and:
			DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
			assertThatJson(parsedJson).field("['status']").isEqualTo("NOT_OK");
	}

契約で記述された機能の実装がまだ存在しないため、テストは失敗します。

これらを通過させるには、HTTP リクエストまたはメッセージを処理するための正しい実装を追加する必要があります。また、自動生成テストの基本テストクラスをプロジェクトに追加する必要があります。このクラスはすべての自動生成テストによって拡張され、テストの実行に必要なすべてのセットアップ情報 (RestAssuredMockMvc コントローラーのセットアップやメッセージングテストのセットアップなど) を含む必要があります。

pom.xml からの次の例は、基本テストクラスを指定する方法を示しています。

<build>
        <plugins>
            <plugin>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-contract-maven-plugin</artifactId>
                <version>2.1.2.RELEASE</version>
                <extensions>true</extensions>
                <configuration>
                    <baseClassForTests>com.example.contractTest.BaseTestClass</baseClassForTests> (1)
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
1baseClassForTests 要素を使用すると、基本テストクラスを指定できます。これは、spring-cloud-contract-maven-plugin 内の configuration 要素の子である必要があります。

次の例は、最小限の (ただし機能する) 基本テストクラスを示しています。

package com.example.contractTest;

import org.junit.Before;

import io.restassured.module.mockmvc.RestAssuredMockMvc;

public class BaseTestClass {

	@Before
	public void setup() {
		RestAssuredMockMvc.standaloneSetup(new FraudController());
	}
}

テストを機能させるために実際に必要なのは、この最小限のクラスだけです。これは、自動生成されたテストが接続される開始場所として機能します。

これで実装に進むことができます。そのためには、まずデータクラスが必要で、それをコントローラーで使用します。次のリストはデータクラスを示しています。

package com.example.Test;

import com.fasterxml.jackson.annotation.JsonProperty;

public class LoanRequest {

	@JsonProperty("client.id")
	private String clientId;

	private Long loanAmount;

	public String getClientId() {
		return clientId;
	}

	public void setClientId(String clientId) {
		this.clientId = clientId;
	}

	public Long getLoanAmount() {
		return loanAmount;
	}

	public void setLoanRequestAmount(Long loanAmount) {
		this.loanAmount = loanAmount;
	}
}

前述のクラスは、パラメーターを格納できるオブジェクトを提供します。契約内のクライアント ID は client.id であるため、@JsonProperty("client.id") パラメーターを使用してそれを clientId フィールドにマップする必要があります。

ここで、次のリストに示すコントローラーに進むことができます。

package com.example.docTest;

import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class FraudController {

	@PutMapping(value = "/fraudcheck", consumes="application/json", produces="application/json")
	public String check(@RequestBody LoanRequest loanRequest) { (1)

		if (loanRequest.getLoanAmount() > 10000) { (2)
			return "{fraudCheckStatus: FRAUD, rejection.reason: Amount too high}"; (3)
		} else {
			return "{fraudCheckStatus: OK, acceptance.reason: Amount OK}"; (4)
		}
	}
}
1 受信パラメーターを LoanRequest オブジェクトにマップします。
2 ご希望の融資金額が多すぎるかどうかを確認します。
3 それが多すぎる場合は、テストが期待する JSON (ここでは単純な文字列で作成された) を返します。
4 量がいつ許容されるかを把握するテストがあれば、それをこの出力と一致させることができます。

FraudController は非常にシンプルです。ログ記録やクライアント ID の検証など、さらに多くのことを行うことができます。

実装とテスト基本クラスが配置されると、テストに合格し、アプリケーションとスタブアーティファクトの両方がビルドされ、ローカル Maven リポジトリにインストールされます。次の例に示すように、ローカルリポジトリへのスタブ jar のインストールに関する情報がログに表示されます。

[INFO] --- spring-cloud-contract-maven-plugin:1.0.0.BUILD-SNAPSHOT:generateStubs (default-generateStubs) @ http-server ---
[INFO] Building jar: /some/path/http-server/target/http-server-0.0.1-SNAPSHOT-stubs.jar
[INFO]
[INFO] --- maven-jar-plugin:2.6:jar (default-jar) @ http-server ---
[INFO] Building jar: /some/path/http-server/target/http-server-0.0.1-SNAPSHOT.jar
[INFO]
[INFO] --- spring-boot-maven-plugin:1.5.5.BUILD-SNAPSHOT:repackage (default) @ http-server ---
[INFO]
[INFO] --- maven-install-plugin:2.5.2:install (default-install) @ http-server ---
[INFO] Installing /some/path/http-server/target/http-server-0.0.1-SNAPSHOT.jar to /path/to/your/.m2/repository/com/example/http-server/0.0.1-SNAPSHOT/http-server-0.0.1-SNAPSHOT.jar
[INFO] Installing /some/path/http-server/pom.xml to /path/to/your/.m2/repository/com/example/http-server/0.0.1-SNAPSHOT/http-server-0.0.1-SNAPSHOT.pom
[INFO] Installing /some/path/http-server/target/http-server-0.0.1-SNAPSHOT-stubs.jar to /path/to/your/.m2/repository/com/example/http-server/0.0.1-SNAPSHOT/http-server-0.0.1-SNAPSHOT-stubs.jar

これで、変更をマージし、アプリケーションとスタブアーティファクトの両方をオンラインリポジトリに公開できるようになります。

コンシューマー側

統合テストで Spring Cloud Contract スタブランナーを使用して、実際のサービスをシミュレートする実行中の WireMock インスタンスまたはメッセージングルートを取得できます。

まず、次のように依存関係を Spring Cloud Contract Stub Runner に追加します。

<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
	<scope>test</scope>
</dependency>

Maven リポジトリにインストールされているプロデューサー側のスタブは、次の 2 つの方法のいずれかで取得できます。

  • Producer 側のリポジトリをチェックアウトし、次のコマンドを実行して契約を追加し、スタブを生成します。

    $ cd local-http-server-repo
    $ ./mvnw clean install -DskipTests
    プロデューサー側の契約実装がまだ整っていないため、テストはスキップされ、自動生成された契約 テストは失敗します。
  • リモートリポジトリから既存のプロデューサーサービススタブを取得します。これを行うには、次の例に示すように、スタブアーティファクト ID とアーティファクトリポジトリ URL を Spring Cloud Contract Stub Runner プロパティとして渡します。

    stubrunner:
      ids: 'com.example:http-server-dsl:+:stubs:8080'
      repositoryRoot: https://repo.spring.io/libs-snapshot

これで、テストクラスに @AutoConfigureStubRunner のアノテーションを付けることができます。次の例に示すように、アノテーションで、Spring Cloud Contract Stub Runner の group-id および artifact-id を指定して、コラボレーターのスタブを実行します。

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment=WebEnvironment.NONE)
@AutoConfigureStubRunner(ids = {"com.example:http-server-dsl:+:stubs:6565"},
		stubsMode = StubRunnerProperties.StubsMode.LOCAL)
public class LoanApplicationServiceTests {
	. . .
}
オンラインリポジトリからスタブをダウンロードする場合は REMOTEstubsMode を使用し、オフライン作業には LOCAL を使用します。

統合テストでは、コラボレーターサービスによって発行されることが予想される HTTP レスポンスまたはメッセージのスタブバージョンを受信できます。ビルドログに次のようなエントリが表示されます。

2016-07-19 14:22:25.403  INFO 41050 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Desired version is + - will try to resolve the latest version
2016-07-19 14:22:25.438  INFO 41050 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Resolved version is 0.0.1-SNAPSHOT
2016-07-19 14:22:25.439  INFO 41050 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Resolving artifact com.example:http-server:jar:stubs:0.0.1-SNAPSHOT using remote repositories []
2016-07-19 14:22:25.451  INFO 41050 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Resolved artifact com.example:http-server:jar:stubs:0.0.1-SNAPSHOT to /path/to/your/.m2/repository/com/example/http-server/0.0.1-SNAPSHOT/http-server-0.0.1-SNAPSHOT-stubs.jar
2016-07-19 14:22:25.465  INFO 41050 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Unpacking stub from JAR [URI: file:/path/to/your/.m2/repository/com/example/http-server/0.0.1-SNAPSHOT/http-server-0.0.1-SNAPSHOT-stubs.jar]
2016-07-19 14:22:25.475  INFO 41050 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Unpacked file to [/var/folders/0p/xwq47sq106x1_g3dtv6qfm940000gq/T/contracts100276532569594265]
2016-07-19 14:22:27.737  INFO 41050 --- [           main] o.s.c.c.stubrunner.StubRunnerExecutor    : All stubs are now running RunningStubs [namesAndPorts={com.example:http-server:0.0.1-SNAPSHOT:stubs=8080}]