传统方式下,微服务的集成以及测试都是一件很头痛的事情。其实在微服务概念还没有出现之前,在 SOA 流行的时候,就有人提出了消费者驱动契约(Consumer Driven Contract,CDC)的概念。微服务流行后,服务的集成和集成测试成了不得不解决问题,于是出现了基于消费者驱动契约的测试工具,最流行的应该就是 Pact,还有就是今天我们要说的 Spring Cloud Contract。
契约测试是一种软件测试方法,它用于验证服务或组件之间的交互是否符合预定义的契约(协议)。契约定义了服务或组件之间交互的格式,包括请求的格式、响应的格式,以及可能的错误场景。
契约测试主要应用于微服务架构中,用于确保服务之间的通信符合预期。在微服务架构中,一个服务的改动可能会影响到其他依赖于该服务的消费者服务。通过契约测试,提供者可以在发布新版本之前确保其改动不会破坏现有的消费者,同时消费者也可以验证其对提供者的调用是否正确。
常见的契约测试工具包括Spring Cloud Contract、Pact等。
Spring Cloud Contract 是一个框架,旨在帮助开发者编写、测试和验证微服务之间的契约。它主要用于确保服务提供者和消费者之间的通信遵循相同的API规范,从而避免由于接口不一致导致的通信问题。Spring Cloud Contract 是 Spring Cloud 生态系统的一部分,它与Spring Boot、Spring Cloud等其他组件兼容且易于集成。
Spring Cloud Contract 主要提供以下功能:
契约定义:使用Groovy DSL或者YAML来定义服务的请求和响应格式,包括HTTP状态码、请求方法、URL、请求参数、响应内容等。
自动生成测试代码:根据定义的契约,自动生成服务提供者端的测试用例,帮助开发者验证服务实现是否满足契约。
生成服务测试桩(Stubs):为服务提供者生成测试桩,允许消费者在本地进行集成测试,而无需调用远程的实际服务。
集成到构建和测试流程:通过Maven或Gradle插件,将Spring Cloud Contract集成到项目的构建和测试流程中,确保服务持续遵循契约。
上面为了解释 Spring Cloud Contract 和契约测试。引出了一堆概念和名词,不要慌,其实通俗的理解,Spring Cloud Contract就是一个工具。我举个例子应该很好理解。
当我们开发微服务应用时,一个常见的问题是如何确保服务之间正确地进行通信。例如,服务A可能需要向服务B发出请求,它期望得到特定格式的响应。如果服务B发生更改,并以不同的方式响应,那么服务A可能会出现问题,因为它并没有得到它期待的数据。
这就是Spring Cloud Contract可以派上用场的地方。使用Spring Cloud Contract,开发者可以创建一个“契约”来定义服务之间交互的预期行为。这个契约可以描述请求的格式,以及对应响应的格式。然后,Spring Cloud Contract会使用这个契约生成测试,以确保服务按照预期进行通信。
这样,如果服务B发生变化并且不再满足契约,测试就会失败,开发人员就会知道他们需要修复服务B,或者更新服务A以适应新的响应格式。
Spring Cloud Contract的核心组件包括Contract DSL、Contract Broker和Contract Verifier。
Contract DSL(Domain-Specific Language)是一种专门用于定义服务之间交互契约的领域特定语言。在Spring Cloud Contract中,我们可以使用Groovy DSL或YAML格式来编写契约。这些契约定义了服务之间的请求和响应,包括请求方法、URL、请求参数、响应状态码和响应内容等信息。
HTTP请求/响应契约的示例:
Groovy DSL示例:
Contract.make {
request {
method 'GET'
urlPath('/api/users/') {
queryParameters {
parameter 'name', 'John'
}
}
}
response {
status 200
body([
id: 1,
name: 'John',
age: 30
])
headers {
header 'Content-Type': 'application/json'
}
}
}
YAML示例:
request:
method: GET
urlPath: "/api/users/"
queryParameters:
- key: name
value: John
response:
status: 200
body:
id: 1
name: John
age: 30
headers:
Content-Type: "application/json"
对于消息传递格式的契约,可以定义消息的发送和接收,以及消息的内容和相关属性。
消息传递契约示例:
Groovy DSL示例:
Contract.make {
label 'sendUserCreatedMessage'
outputMessage {
sentTo 'userCreatedChannel'
body([
id: 1,
name: 'John',
age: 30
])
headers {
header 'contentType': 'application/json'
}
}
}
YAML示例:
label: "sendUserCreatedMessage"
outputMessage:
sentTo: "userCreatedChannel"
body:
id: 1
name: John
age: 30
headers:
contentType: "application/json"
这些定义的契约将被Spring Cloud Contract用于生成服务提供者的测试用例和测试桩(stubs),以验证服务实现是否符合契约,并使得消费者可以在不调用实际服务的情况下进行测试。
Contract Broker是一个集中存储和共享契约的仓库,它的主要目的是为服务提供者和消费者提供一个统一的契约来源,以确保它们都遵循相同的API规范。当契约发生变更时,Contract Broker会通知所有相关的服务消费者,使它们能够更新自己的测试用例和测试桩(stubs)。
Spring Cloud Contract支持多种仓库作为Contract Broker,例如:
Git仓库:可以将契约文件存储在Git仓库中,利用Git的版本控制功能跟踪契约的变更,并将其与服务的源代码一起维护。这样,当契约发生变更时,消费者只需要拉取最新的契约文件,即可更新测试用例和测试桩。
Nexus:Nexus是一个流行的依赖管理仓库,可以用来存储Java库和其他构建工件。通过将契约文件打包为一个JAR文件并发布到Nexus仓库,消费者可以将其作为依赖添加到项目中,并在构建过程中自动下载最新的契约。这样,当契约发生变更时,消费者只需要更新依赖版本,即可获取最新的契约文件。
使用Contract Broker的好处在于:
为了使用Contract Broker,服务提供者需要在构建和发布时将契约文件上传到仓库,而服务消费者需要在构建和测试时从仓库下载契约文件。这可以通过Maven或Gradle等构建工具的插件来实现。
首先,服务提供者会定义一个契约,描述这个API的请求和响应:
Contract.make {
request {
method 'GET'
urlPath('/users/1')
}
response {
status 200
body([
id: 1,
name: 'John',
email: 'john@example.com'
])
headers {
header 'Content-Type': 'application/json'
}
}
}
然后,服务提供者会将这个契约文件保存在Git仓库中,作为Contract Broker。消费者可以从这个仓库中获取契约文件,生成测试用例和测试桩。
当服务提供者需要修改API时,比如添加一个新的字段"age",那么它需要先更新契约文件,再修改实现代码:
Contract.make {
request {
method 'GET'
urlPath('/users/1')
}
response {
status 200
body([
id: 1,
name: 'John',
email: 'john@example.com',
age: 30
])
headers {
header 'Content-Type': 'application/json'
}
}
}
更新契约文件之后,服务提供者将新的契约文件推送到Git仓库。这时,所有的消费者都会接收到契约变更的通知,然后更新自己的测试用例和测试桩,保证与新的API兼容。
Contract Verifier是Spring Cloud Contract中的一个核心组件,它的主要职责是验证服务提供者的实现是否满足定义好的契约。为了实现这个目标,Contract Verifier会自动根据契约生成测试用例和测试桩。下面是Contract Verifier的工作流程:
自动生成测试用例:当服务提供者的代码发生变更时,Contract Verifier会根据契约文件生成对应的测试用例。这些测试用例会覆盖契约中定义的请求和响应,以确保服务提供者的实现与契约一致。例如,对于上面定义的用户信息API,Contract Verifier会生成一个测试用例,模拟发送一个GET请求到"/users/1",并验证返回的JSON对象包含正确的字段和值。
运行测试用例:在生成测试用例之后,Contract Verifier会自动运行这些测试用例,以检查服务提供者的实现是否满足契约。如果测试用例运行失败,那么说明服务提供者的实现与契约不一致,需要修改代码以满足契约要求。
生成测试桩(stubs):除了验证服务提供者的实现,Contract Verifier还负责生成测试桩。测试桩是一种模拟服务提供者行为的工具,它根据契约文件生成一个虚拟的服务实现,使得服务消费者可以在本地进行集成测试,而无需调用真实的服务。测试桩的生成可以通过WireMock等工具来实现,它会根据契约文件生成一个HTTP服务器,监听指定的端口,并根据契约定义的请求和响应来处理请求。
假设我们有一个用户服务,提供了一个用于获取用户信息的REST API,我们可以编写以下契约来描述这个API:
Contract.make {
request {
method 'GET'
urlPath('/users/1')
}
response {
status 200
body([
id: 1,
name: 'John',
email: 'john@example.com'
])
headers {
header 'Content-Type': 'application/json'
}
}
}
与此同时,我们在服务提供者的项目中启用Spring Cloud Contract Verifier,它将自动根据这个契约生成测试用例。
生成的测试用例可能会像下面这样:
public class ContractVerifierTest extends ContractVerifierBase {
@Test
public void validate_getUser_1() throws Exception {
// given:
MockMvcRequestSpecification request = given()
.header("Content-Type", "application/json");
// when:
ResponseOptions response = given().spec(request)
.get("/users/1");
// 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("['id']").isEqualTo(1);
assertThatJson(parsedJson).field("['name']").isEqualTo("John");
assertThatJson(parsedJson).field("['email']").isEqualTo("john@example.com");
}
}
然后,当运行测试的时候,Contract Verifier会执行这个测试用例,验证API的实现是否符合契约的要求。
另一方面,Contract Verifier还会生成一个测试桩。服务消费者可以使用这个测试桩进行集成测试,而不需要调用真实的服务。例如,消费者可以启动一个本地的HTTP服务器,模拟用户服务的行为。当收到"/users/1"的GET请求时,服务器返回预定义的响应:
{
"id": 1,
"name": "John",
"email": "john@example.com"
}
这样,消费者就可以在不依赖于实际服务的情况下,进行集成测试,提高测试的速度和稳定性。
设我们有一个用户服务(UserService),提供了一个用于获取用户信息的REST API。我们将为这个API创建契约,并在服务提供者和服务消费者中进行测试。
创建一个名为user-service
的Maven项目,添加Spring Boot依赖。在pom.xml
中添加以下内容:
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
dependencies>
在pom.xml
中添加Spring Cloud Contract Maven插件:
<plugin>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-contract-maven-pluginartifactId>
<version>${spring-cloud-contract.version}version>
<extensions>trueextensions>
<configuration>
<baseClassForTests>
com.example.userservice.BaseTestClass
baseClassForTests>
configuration>
plugin>
在user-service
项目的src/test/resources/contracts
目录下创建一个名为getUser.groovy
的文件,编写如下契约:
Contract.make {
request {
method 'GET'
url '/users/1'
}
response {
status 200
body([
id: 1,
name: 'John',
email: 'john@example.com'
])
headers {
contentType(applicationJson())
}
}
}
在user-service
项目中创建一个UserController
类,实现/users/{id}
接口。
@RestController
public class UserController {
@GetMapping("/users/{id}")
public ResponseEntity<User> getUser(@PathVariable("id") Long id) {
User user = new User(1L, "John", "john@example.com");
return ResponseEntity.ok(user);
}
}
运行mvn clean install
命令,Spring Cloud Contract会自动生成测试代码和stubs。
创建一个名为user-client
的Maven项目,添加Spring Boot依赖。
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-contract-stub-runnerartifactId>
<version>${spring-cloud.version}version>
<scope>testscope>
dependency>
dependencies>
在user-client
项目中创建UserServiceClientTest
类,并使用@AutoConfigureStubRunner
注解启动stubs。
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
@AutoConfigureStubRunner(ids = {"com.example:user-service:+:stubs:8080"}, workOffline = true)
public class UserServiceClientTest {
@Autowired
private UserServiceClient userServiceClient;
@Test
public void testGetUser() {
User user = userServiceClient.getUser(1L);
assertNotNull(user);
assertEquals(1L, user.getId().longValue());
assertEquals("John", user.getName());
assertEquals("john@example.com", user.getEmail());
}
}
运行user-client
项目中的测试用例,验证服务消费者是否正确地调用了服务提供者的API。
将user-service
和user-client
项目的契约文件及生成的stubs添加到的源代码仓库,并将其集成到的CI/CD流程中。这样,每当有新的代码提交,CI/CD系统就会自动运行契约测试,保证新的代码仍然满足契约的要求。同时,服务消费者也可以从CI/CD系统获取最新的stubs,进行集成测试。
Spring Cloud Contract支持在运行时动态生成契约。可以在契约中使用Groovy的动态特性,例如,使用循环、条件判断和变量等。这使得可以创建更加复杂和灵活的契约,以适应各种测试场景。
例如,可以使用value()
函数来生成随机数据:
在这个示例中,$(value(consumer(regex(uuid())), producer('12345')))
表示在消费者端,id
字段应该符合UUID的格式,在提供者端,id
字段的值应该是12345
。
Contract.make {
request {
method 'POST'
url '/users'
body([
id: $(value(consumer(regex(uuid())), producer('12345'))),
name: 'John',
email: 'john@example.com'
])
headers {
contentType(applicationJson())
}
}
response {
status 201
}
}
在消费方驱动契约测试中,服务的消费者(而不是提供者)负责定义契约。这样可以确保服务提供者的实现满足消费者的需求。Spring Cloud Contract支持消费方驱动契约测试,可以帮助更好地协调服务消费者和提供者之间的交互。
以下教程将演示如何在Spring Boot项目中使用WireMock和RestDocs进行契约测试。
场景: 假设我们有一个用户服务(UserService),提供了一个用于获取用户信息的REST API。我们将使用WireMock模拟这个API的行为,并使用RestDocs生成API文档。然后,我们将使用Spring Cloud Contract进行契约测试。
步骤1:创建Spring Boot项目
创建一个名为UserService
的Spring Boot项目,并添加以下依赖:
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>com.github.tomakehurstgroupId>
<artifactId>wiremock-jre8artifactId>
<version>2.31.0version>
<scope>testscope>
dependency>
<dependency>
<groupId>org.springframework.restdocsgroupId>
<artifactId>spring-restdocs-mockmvcartifactId>
<scope>testscope>
dependency>
dependencies>
步骤2:编写测试用例
在UserService
项目中创建一个名为UserServiceTest
的测试类,使用WireMock模拟REST API,使用RestDocs生成API文档。
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class UserServiceTest {
@Autowired
private MockMvc mockMvc;
private WireMockServer wireMockServer;
@Before
public void setup() {
wireMockServer = new WireMockServer(8080);
wireMockServer.start();
wireMockServer.stubFor(get(urlEqualTo("/users/1"))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
.withBody("{\"id\": 1, \"name\": \"John\", \"email\": \"john@example.com\"}")));
}
@After
public void tearDown() {
wireMockServer.stop();
}
@Test
public void testGetUser() throws Exception {
mockMvc.perform(get("/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id", is(1)))
.andExpect(jsonPath("$.name", is("John")))
.andExpect(jsonPath("$.email", is("john@example.com")))
.andDo(document("get-user",
responseFields(
fieldWithPath("id").description("The user's ID"),
fieldWithPath("name").description("The user's name"),
fieldWithPath("email").description("The user's email"))));
}
}
步骤3:运行测试并生成API文档
运行测试用例,生成的API文档将保存在target/generated-snippets
目录中。
步骤4:将生成的API文档转换为契约
将生成的API文档复制到src/test/resources/contracts
目录下,然后使用Spring Cloud Contract将API文档转换为契约。
步骤5:生成测试代码和stubs
运行mvn clean install
命令,Spring Cloud Contract会自动生成基于契约的测试代码和stubs。
步骤6:使用契约stubs进行集成测试
服务消费方可以使用生成的stubs进行集成测试。例如,可以使用Spring Cloud Contract Stub Runner来运行stubs:
@RunWith(SpringRunner.class)
@AutoConfigureStubRunner(ids = {"com.example:UserService:+:stubs:8080"}, workOffline = true)
public class UserServiceClientTest {
// 测试用例...
}
Spring Cloud Contract的版本控制和兼容性检查功能是非常重要的。这些功能可以确保服务提供者在修改API实现时,不会破坏服务消费者的期望。下面提供一些使用这些功能的基本步骤。
1. 为契约指定版本号:
在Spring Cloud Contract中,契约的版本由服务提供者的项目版本决定。可以在项目的pom.xml中设置项目版本:
<version>1.0.0version>
然后,可以在服务消费者中引用特定版本的契约:
@AutoConfigureStubRunner(ids = {"com.example:UserService:1.0.0:stubs:8080"})
public class UserServiceClientTest {
...
}
2. 检查契约兼容性:
当修改契约时,需要确保新的契约与旧的契约兼容。这样,才能确保服务消费者不会因为服务提供者的修改而破产。
可以使用Spring Cloud Contract的兼容性检查功能来检查契约的兼容性。只需要运行mvn clean install
命令,Spring Cloud Contract会自动生成测试代码,然后运行这些测试代码来检查契约兼容性。如果新的契约不兼容,测试会失败,需要根据失败的测试修正的契约。
契约兼容性的主要考虑因素是:新的契约是否满足所有旧的契约的期望。如果新的契约添加了一些新的字段或新的API,但仍然满足旧的契约的期望,那么新的契约就是与旧的契约兼容的。
1. 契约管理策略
集中式契约仓库 将所有契约存储在一个集中的仓库中,这样可以方便地管理和查找契约。此外,这有助于确保团队成员遵循相同的契约编写规范和约定。
明确的命名和分组 为契约文件使用明确的命名和分组策略,例如按服务名称、功能模块或版本来组织契约。这可以帮助团队更容易地理解和查找契约。
编写可读性强的契约 尽量使契约易于理解,如使用描述性的字段名、清晰的注释和示例。这有助于提高团队成员的协作效率和契约的可维护性。
2. 如何处理不同的服务提供方或消费方版本
使用语义化版本控制 采用语义化版本控制策略(如MAJOR.MINOR.PATCH),可以在版本号中反映出契约的兼容性。例如,当引入兼容性变更时,增加主版本号;当引入向后兼容的新功能时,增加次版本号。
灵活的契约引用 消费方可以使用范围(如1.x.x
)或通配符(如1.0.+
)来引用契约,以便自动接收兼容的版本更新。但请注意,这种做法可能增加风险,因为它需要更多的信任和测试来确保兼容性。
3. 测试覆盖率和维护性的平衡
编写端到端测试 端到端测试可以确保服务提供者和消费者之间的实际交互与契约一致。然而,端到端测试通常较慢,可能受到外部依赖的影响。因此,请在测试套件中保持合理的端到端测试数量,避免过度依赖这类测试。
聚焦关键场景和边界条件 聚焦关键场景和边界条件的测试,确保覆盖业务逻辑的核心部分。同时,保持测试用例的简洁和模块化,以便于维护。
4. 与其他测试框架和工具集成:
与CI/CD集成: 将契约测试集成到持续集成(CI)和持续交付(CD)流程中,确保每次代码提交或部署都通过契约测试。这有助于及时发现并修复问题,减少错误对生产环境的影响。
与API文档工具集成: 使用API文档工具(如Swagger、Spring RestDocs)与Spring Cloud Contract集成,可以在生成API文档的同时进行契约测试。这样可以确保API文档始终与实际服务实现保持一致。
demo地址
https://spring.io/projects/spring-cloud-contract
https://www.infoq.com/articles/contract-testing-spring-cloud-contract/
https://github.com/wangshuai67/spring-cloud-contract-samples
https://cloud.spring.io/spring-cloud-contract/multi/multi_spring-cloud-contract.html
http://blog.didispace.com/spring-cloud-contract-summary-hzf/
http://www.infoq.com/cn/news/2017/04/spring-cloud-contract