先上个导图
一 什么是 CDC
消费者驱动的契约测试(Consumer-Driven Contracts,简称CDC),是指从消费者业务实现的角度出发,驱动出契约,再基于契约,对提供者验证的一种测试方式。
通常在开发过程中主要是由服务提供方来提供约定接口,虽然提供方架构和接口进行调整后会通知消费者,但还是会存在风险的。而在微服务架构中,不同的服务可能会由不同的团队维护,这种情况下对接口的开发和维护也将会带来一些问题。
为了解决这些问题呢,Ian Robinson提出了一个以服务消费者定义契约为驱动的开发模式:“Consumer-Driver Contracts(CDC)”,就是:消费者驱动契约。而 CDC 的总体流程是,消费者定义了它们期望的 API/消息 是什么样子,这种期望就被称为契约,提供者需要编写验证这些契约并生成 stubs 供生产者重复使用。
Spring Cloud Contract 则是 CDC 的一种具体实现。
二 微服务架构下测试存在的一些问题
假设我们有一个由多个 services 组成的系统,我们在没有 CDC 的情况下对 v1 这个 service 进行测试:
- 对于端到端的测试
- 为了测试一个 service,我们需要将所有的 services 和对应的 databases 都部署起来,一旦我们有很多的 service 后,这个成本非常大
- 对于单元/集成测试
- 在编写测试时候需要 mock 大量的数据
- 很难应对服务端调整架构或接口后带来的问题
三 Spring Cloud Contract 的解决方案
Spring Cloud Contract 可以为我们生成一个可被验证的 Stub Runner,这样我们就可以在不启动其它 services 的同时及时的获取反馈信息,因为在双方都准守契约的情况下这个 Stub Runner 就相当于我们启动了对应的 service
好处:
- 确保 WireMock/message stubs 是完全按照实际的服务端实现的
- 推广 ATDD 方法和为服务架构
- 当提供者发布最新的契约时,双方都可以快速发现
- 既可以对自身服务进行测试,也可以生成 stub 提供给其它 service 进行调用
四 Quickly Start
server side
Spring Cloud Contract 采用 Groovy DSL 来定义契约,当然了你也可以采用 yml 来编写。现在我们来实现一个简单的 stub ,包含两个 API,根据 id 获取用户信息以及添加一条用户信息
1 编写 contracts
add user
import org.springframework.cloud.contract.spec.Contract
Contract.make {
name 'should_return_status_created' //最终生成测试时的方法名,默认是文件名
request {
method 'post'
url '/users'
body([
id : 1,
username: 'acey'
])
headers {
contentType(applicationJsonUtf8())
}
}
response {
status 201
}
}
get user
import org.springframework.cloud.contract.spec.Contract
Contract.make {
name 'should_return_user' //最终生成测试时的方法名,默认是文件名
request {
method 'GET'
url value(
consumer(regex('/users/\\d+')),
producer('/users/1'))
}
response {
status 200
body([
id : 1L,
username: "acey",
])
testMatchers {
jsonPath('$.id', byRegex(number()))
jsonPath('$.username',byRegex(onlyAlphaUnicode()))
}
headers {
contentType(applicationJsonUtf8())
}
}
}
其中:
url value(
consumer(regex('/users/\\d+')),
producer('/users/1'))
当生成 stub 后,
-
consumer
表示客户端在发送该请求时可以进行正则匹配,只需要保证
users
后跟的是一个数字即可 -
producer
表示当客服端发送了符合正则的请求后,所以符合的请求都将调用/users/1
这个接口
而对于 response 也是类似
response {
status 200
body([
id : 1L,
username: "acey",
])
testMatchers {
jsonPath('$.id', byRegex(number()))
jsonPath('$.username',byRegex(onlyAlphaUnicode()))
}
headers {
contentType(applicationJsonUtf8())
}
}
body
里面的数据是客户端在请求后返回的具体的结果
而 testMatchers
则是针对服务端在自身测试时进行一个模糊验证
2 生成/运行测试
使用 ./gradlew generateContractTests
生成 contracts 对应的测试,当然你也可以直接 build / test 。生成的测试长这样
public class UserTest extends UserBase {
@Rule
public Base base = new Base();
@Test
public void validate_should_return_status_created() throws Exception {
// given:
MockMvcRequestSpecification request = given()
.header("Content-Type", "application/json;charset=UTF-8")
.body("{\"id\":1,\"username\":\"acey\"}");
// when:
ResponseOptions response = given().spec(request)
.post("/users");
// then:
assertThat(response.statusCode()).isEqualTo(201);
}
@Test
public void validate_should_return_user() throws Exception {
// given:
MockMvcRequestSpecification request = given();
// when:
ResponseOptions response = given().spec(request)
.get("/users/1");
// then:
assertThat(response.statusCode()).isEqualTo(200);
assertThat(response.header("Content-Type")).matches("application/json;charset=UTF-8.*");
// and:
DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
// and:
assertThat(parsedJson.read("$.id", String.class)).matches("-?\\d*(\\.\\d+)?");
assertThat(parsedJson.read("$.username", String.class)).matches("[\\p{L}]*");
}
}
有必要再来看一下目录结构
更多配置信息
3 生成 stub
执行 ./gradlew install
会在本机的 .m2/repository
下生成一个 **-stub.jar
当然你也可以发布发布到一个 remote repository
client side
当有了 stub 之后,在 client side 去调用就比较简单了,在这就只演示下 get user
这个 API (当然你也可以采用契约测试进行测试)
UserServiceTest.class
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureStubRunner(ids = "com.acey:server:+:stubs:10001",
workOffline = true)
@ActiveProfiles("test")
public class UserServiceTest {
@Autowired
private UserService userService;
@Test
public void getUserInfo() throws Exception {
ResponseEntity userInfo = userService.getUserInfo(1L);
Map user = (Map) userInfo.getBody();
assertEquals(HttpStatus.OK, userInfo.getStatusCode());
assertEquals("acey", user.get("username"));
}
}
其中
com.acey:server:+:stubs:10001
运行的时候会在我们本地 `.m2/repository` 目录下去找 com.acey:server:+:stubs 这个这个 jar 文件
并 run 起来,端口是 10001,`+` 表示会自动找最新的版本
UserService.class
@Service
public class UserService {
@Value("${userCenter}")
private String userCenterUrl;
public ResponseEntity getUserInfo(Long userId) {
String getUsersUrl = userCenterUrl + "/users/" + userId;
RestTemplate template = new RestTemplate();
ResponseEntity
application-test.yml
userCenter: http://localhost:10001
五 独立部署
1 首先下载stub-runner.jar
wget -O stub-runner.jar 'https://search.maven.org/remotecontent?filepath=org/springframework/cloud/spring-cloud-contract-stub-runner-boot/2.1.1.RELEASE/spring-cloud-contract-stub-runner-boot-2.1.1.RELEASE.jar'
2. 在同路径创建配置文件application.yml,并添加stubrunner的配置,如下
stubrunner:
ids:
# - [groupId]:artifactId:[version]:[classify]:[port]
# 指定运行在8081端口
- com.jtj.cloud:spring-contract-example:1.0.0:+:8081
# 该模式下会从远程maven仓库缓存到本地
stubs-mode: local[remote 每次拉取远程]
3. 通过java -jar stub-runner.jar运行,访问对应端口请求数据
完整 Demo 地址
参考:
- 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