Spring Cloud Contract サービス間テスト

このガイドでは、契約スタブを使用して Spring REST アプリケーションを作成し、他の Spring アプリケーション内で契約を使用するプロセスを順を追って説明します。Spring Cloud Contract プロジェクト (英語)

構築するもの

2 つのマイクロサービスをセットアップし、1 つは契約を提供し、もう 1 つはこの契約を使用して、契約プロバイダーサービスへの統合が仕様に沿っていることを確認します。将来、プロデューサーサービスの契約が変更された場合、コンシューマーサービスのテストは失敗し、潜在的な非互換性をキャッチできません。

必要なもの

本ガイドの完成までの流れ

ほとんどの Spring 入門ガイドと同様に、最初から始めて各ステップを完了するか、すでに慣れている場合は基本的なセットアップステップをバイパスできます。いずれにしても、最終的に動作するコードになります。

最初から始めるには、Gradle でビルドするに進みます。

基本スキップするには、次の手順を実行します。

完了したときは、gs-contract-rest/complete のコードに対して結果を確認できます。

Gradle でビルドする

Gradle でビルドする

最初に、基本的なビルドスクリプトを設定します。Spring を使用してアプリをビルドする場合、好きなビルドシステムを使用できますが、Gradle (英語) および Maven (英語) を操作するために必要なコードはここに含まれています。どちらにも詳しくない場合は、Gradle で Java プロジェクトの構築または Maven で Java プロジェクトの構築を参照してください。

ディレクトリ構造を作成する

選択したプロジェクトディレクトリで、次のサブディレクトリ構造を作成します。たとえば、*nix システム上の mkdir -p src/main/java/hello の場合:

└── src
    └── main
        └── java
            └── hello

Gradle ビルドファイルを作成する

contract-rest-service/build.gradle

plugins {
  id 'org.springframework.boot' version '3.5.14'
  id 'org.springframework.cloud.contract' version '4.3.3'
  id 'io.spring.dependency-management' version '1.1.7'
  id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'

java {
  sourceCompatibility = 17
  targetCompatibility = 17
}

repositories {
  mavenCentral()
}

dependencies {
  implementation('org.springframework.boot:spring-boot-starter-web')
  testImplementation('org.springframework.boot:spring-boot-starter-test')
  testImplementation('org.springframework.cloud:spring-cloud-starter-contract-verifier')
  testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

dependencyManagement {
  imports {
    mavenBom "org.springframework.cloud:spring-cloud-dependencies:2025.0.2"
  }
}

copyContracts.enabled = false
contracts {
  failOnNoContracts = false
}

tasks.named('contractTest') {
  useJUnitPlatform()
}

contract-rest-client/build.gradle

plugins {
  id 'org.springframework.boot' version '3.5.14'
  id 'io.spring.dependency-management' version '1.1.7'
  id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'

java {
  sourceCompatibility = 17
  targetCompatibility = 17
}

repositories {
  mavenCentral()
}

dependencies {
  implementation('org.springframework.boot:spring-boot-starter-web')
  testImplementation('org.springframework.boot:spring-boot-starter-test')
  testImplementation('org.springframework.cloud:spring-cloud-starter-contract-stub-runner')
}

dependencyManagement {
  imports {
    mavenBom "org.springframework.cloud:spring-cloud-dependencies:2025.0.2"
  }
}

test {
  useJUnitPlatform()
}

Spring Boot gradle プラグインは多くの便利な機能を提供します。

  • クラスパス上のすべての jar を収集し、単一の実行可能な「ü ber-jar」を構築します。これにより、サービスの実行とトランスポートがより便利になります。

  • public static void main() メソッドを検索して、実行可能なクラスとしてフラグを立てます。

  • Spring Boot の依存関係 [GitHub] (英語) と一致するようにバージョン番号を設定する組み込みの依存関係リゾルバーを提供します。任意のバージョンをオーバーライドできますが、デフォルトで Boot の選択されたバージョンのセットになります。

Maven でビルドする

Maven でビルドする

最初に、基本的なビルドスクリプトを設定します。Spring を使用してアプリをビルドする場合、好きなビルドシステムを使用できますが、Maven (英語) で作業するために必要なコードはここに含まれています。Maven に詳しくない場合は、Maven で Java プロジェクトの構築を参照してください。

ディレクトリ構造を作成する

選択したプロジェクトディレクトリで、次のサブディレクトリ構造を作成します。たとえば、*nix システム上の mkdir -p src/main/java/hello の場合:

└── src
    └── main
        └── java
            └── hello

すぐに始めるために、サーバーおよびクライアントアプリケーションの完全な構成を以下に示します。

contract-rest-service/pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
		 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<groupId>com.example</groupId>
	<artifactId>contract-rest-service</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>jar</packaging>

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>3.5.14</version>
		<relativePath /> <!-- lookup parent from repository -->
	</parent>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<java.version>17</java.version>
		<spring-cloud.version>2025.0.2</spring-cloud.version>
		<spring-cloud-contract.version>4.3.3</spring-cloud-contract.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

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

	<dependencyManagement>
		<dependencies>
			<dependency>
				<groupId>org.springframework.cloud</groupId>
				<artifactId>spring-cloud-dependencies</artifactId>
				<version>${spring-cloud.version}</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
		</dependencies>
	</dependencyManagement>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
			<plugin>
				<groupId>org.springframework.cloud</groupId>
				<artifactId>spring-cloud-contract-maven-plugin</artifactId>
				<version>${spring-cloud-contract.version}</version>
				<extensions>true</extensions>
				<configuration>
					<failOnNoContracts>false</failOnNoContracts>
					<testFramework>JUNIT5</testFramework>
				</configuration>
			</plugin>
		</plugins>
	</build>

</project>

contract-rest-client/pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="https://maven.apache.org/POM/4.0.0" xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="https://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<groupId>com.example</groupId>
	<artifactId>contract-rest-client</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>jar</packaging>

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>3.5.14</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<java.version>17</java.version>
	</properties>

	<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>
		</dependency>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
			<scope>test</scope>
		</dependency>

	</dependencies>

	<dependencyManagement>
		<dependencies>
			<dependency>
				<groupId>org.springframework.cloud</groupId>
				<artifactId>spring-cloud-dependencies</artifactId>
				<version>2025.0.2</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
		</dependencies>
	</dependencyManagement>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>


</project>

Spring Boot Maven プラグインは多くの便利な機能を提供します。

  • クラスパス上のすべての jar を収集し、単一の実行可能な「ü ber-jar」を構築します。これにより、サービスの実行とトランスポートがより便利になります。

  • public static void main() メソッドを検索して、実行可能なクラスとしてフラグを立てます。

  • Spring Boot の依存関係 [GitHub] (英語) と一致するようにバージョン番号を設定する組み込みの依存関係リゾルバーを提供します。任意のバージョンをオーバーライドできますが、デフォルトで Boot の選択されたバージョンのセットになります。

IDE でビルドする

IDE でビルドする

契約プロデューサーサービスの作成

まず、契約を作成するサービスを作成する必要があります。これは、非常にシンプルな REST サービスを提供する通常の Spring Boot アプリケーションです。残りのサービスは、単に JSON で Person オブジェクトを返します。

contract-rest-service/src/main/java/hello/ContractRestServiceApplication.java

package hello;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ContractRestServiceApplication {

  public static void main(String[] args) {
    SpringApplication.run(ContractRestServiceApplication.class, args);
  }
}

REST サービスの契約を作成する

REST サービスの契約は、.groovy スクリプトとして定義できます。この契約は、/person/1 を URL する GET リクエストがある場合、Person エンティティを表すサンプルデータ id=1name=foosurname=bee がコンテンツ型 application/json のレスポンス本文で返されることを指定します。

contract-rest-service/src/test/resources/contracts/hello/find_person_by_id.groovy

import org.springframework.cloud.contract.spec.Contract

Contract.make {
  description "should return person by id=1"

  request {
    url "/person/1"
    method GET()
  }

  response {
    status OK()
    headers {
      contentType applicationJson()
    }
    body (
      id: 1,
      name: "foo",
      surname: "bee"
    )
  }
}

test フェーズでは、groovy ファイルで指定された契約の自動テストクラスが作成されます。これは、Gradle のビルドプラグイン org.springframework.cloud:spring-cloud-contract-gradle-plugin または Maven の org.springframework.cloud:spring-cloud-contract-maven-plugin によってそれぞれ実行されます。自動生成されたテスト java クラスは、hello.BaseClass を継承します。

プラグインを Maven に含めるには、以下を追加する必要があります。

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

テストを実行するには、org.springframework.cloud:spring-cloud-starter-contract-verifier 依存関係をテストスコープに含める必要もあります。

最後に、テスト用の基本クラスを作成します。

contract-rest-service/src/test/java/hello/BaseClass.java

package hello;

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.test.context.bean.override.mockito.MockitoBean;

import static org.mockito.Mockito.when;

@SpringBootTest(classes = ContractRestServiceApplication.class)
public abstract class BaseClass {

  @Autowired
  PersonRestController personRestController;

  @MockitoBean
  PersonService personService;

  @BeforeEach
  public void setup() {
    RestAssuredMockMvc.standaloneSetup(personRestController);
    when(personService.findPersonById(1L))
        .thenReturn(new Person(1L, "foo", "bee"));
  }

}

このステップでは、テストが実行されると、テスト結果が緑になり、REST コントローラーが契約と整合しており、完全に機能するサービスがあることを示します。

単純な Person クエリのビジネスロジックを確認する

モデルクラス Person.java contract-rest-service/src/main/java/hello/Person.java

package hello;

class Person {

  Person(Long id, String name, String surname) {
    this.id = id;
    this.name = name;
    this.surname = surname;
  }

  private Long id;

  private String name;

  private String surname;

  public Long getId() {
    return id;
  }

  public void setId(Long id) {
    this.id = id;
  }

  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }

  public String getSurname() {
    return surname;
  }

  public void setSurname(String surname) {
    this.surname = surname;
  }
}

いくつかの Person エンティティをメモリに追加し、要求されたときにそれを返すサービス Bean PersonService.javacontract-rest-service/src/main/java/hello/PersonService.java

package hello;

import java.util.HashMap;
import java.util.Map;

import org.springframework.stereotype.Service;

@Service
class PersonService {

  private final Map<Long, Person> personMap;
  
  public PersonService() {
    personMap = new HashMap<>();
    personMap.put(1L, new Person(1L, "Richard", "Gere"));
    personMap.put(2L, new Person(2L, "Emma", "Choplin"));
    personMap.put(3L, new Person(3L, "Anna", "Carolina"));
  }
  
  Person findPersonById(Long id) {
    return personMap.get(id);
  }
}

RestController Bean PersonRestController.java は、ID を持つ人物の REST リクエストを受信したときに PersonService Bean を呼び出します。contract-rest-service/src/main/java/hello/PersonRestController.java

package hello;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@RestController
class PersonRestController {

  private final PersonService personService;

  public PersonRestController(PersonService personService) {
    this.personService = personService;
  }

  @GetMapping("/person/{id}")
  public Person findPersonById(@PathVariable("id") Long id) {
    return personService.findPersonById(id);
  }
}

contract-rest-service アプリケーションをテストする

ContractRestServiceApplication.java クラスを Java アプリケーションまたは Spring Boot アプリケーションとして実行します。サービスはポート 8000 で開始する必要があります。

ブラウザー http://localhost:8000/person/1http://localhost:8000/person/2 などでサービスにアクセスします。

契約コンシューマーサービスを作成する

契約プロデューサーサービスの準備が整ったら、提供された契約を使用するクライアントアプリケーションを作成する必要があります。これは非常にシンプルな REST サービスを提供する通常の Spring Boot アプリケーションです。残りのサービスは、照会された個人の名前を含むメッセージを単に返します。Hello Anna

contract-rest-client/src/main/java/hello/ContractRestClientApplication.java

package hello;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@SpringBootApplication
public class ContractRestClientApplication {

  public static void main(String[] args) {
    SpringApplication.run(ContractRestClientApplication.class, args);
  }
}

@RestController
class MessageRestController {

  private final RestTemplate restTemplate;

  MessageRestController(RestTemplateBuilder restTemplateBuilder) {
    this.restTemplate = restTemplateBuilder.build();
  }

  @RequestMapping("/message/{personId}")
  String getMessage(@PathVariable("personId") Long personId) {
    Person person = this.restTemplate.getForObject("http://localhost:8000/person/{personId}", Person.class, personId);
    return "Hello " + person.getName();
  }

}

契約テストを作成する

プロデューサーが提供する契約は、簡単な Spring Test として使用する必要があります。

contract-rest-client/src/test/java/hello/ContractRestClientApplicationTest.java

package hello;

import org.assertj.core.api.BDDAssertions;
import org.junit.jupiter.api.Test;
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.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;

@SpringBootTest
@AutoConfigureStubRunner(
    ids = "com.example:contract-rest-service:0.0.1-SNAPSHOT:stubs:8100",
    stubsMode = StubRunnerProperties.StubsMode.LOCAL
)
public class ContractRestClientApplicationTest {

  @Test
  public void get_person_from_service_contract() {
    // given:
    RestTemplate restTemplate = new RestTemplate();
    
    // when:
    ResponseEntity<Person> personResponseEntity = restTemplate.getForEntity("http://localhost:8100/person/1", Person.class);

    // then:
    BDDAssertions.then(personResponseEntity.getStatusCodeValue()).isEqualTo(200);
    BDDAssertions.then(personResponseEntity.getBody().getId()).isEqualTo(1l);
    BDDAssertions.then(personResponseEntity.getBody().getName()).isEqualTo("foo");
    BDDAssertions.then(personResponseEntity.getBody().getSurname()).isEqualTo("bee");
    
  }
}

このテストクラスは、契約プロデューサーサービスのスタブをロードし、サービスへの統合が契約と整合していることを確認します。

コンシューマーサービスのテストとプロデューサーの契約との間の通信に問題がある場合、テストは失敗し、本番で新しい変更を行う前に問題を修正する必要があります。

contract-rest-client アプリケーションをテストする

ContractRestClientApplication.java クラスを Java アプリケーションまたは Spring Boot アプリケーションとして実行します。サービスはポート 9000 で開始する必要があります。

ブラウザー http://localhost:9000/message/1http://localhost:9000/message/2 などでサービスにアクセスします。

要約

おめでとう! Spring を使用して、REST サービスに契約を宣言させ、コンシューマーサービスをこの契約に合わせるようにしました。

関連事項

次のガイドも役立つかもしれません:

新しいガイドを作成したり、既存のガイドに貢献したいですか? 投稿ガイドラインを参照してください [GitHub] (英語)

すべてのガイドは、コード用の ASLv2 ライセンス、およびドキュメント用の帰属表示、NoDerivatives クリエイティブコモンズライセンス (英語) でリリースされています。

コードを入手する