先上個(gè)導(dǎo)圖
一 什么是 CDC
消費(fèi)者驅(qū)動(dòng)的契約測(cè)試(Consumer-Driven Contracts梳玫,簡(jiǎn)稱CDC)涧至,是指從消費(fèi)者業(yè)務(wù)實(shí)現(xiàn)的角度出發(fā)斤讥,驅(qū)動(dòng)出契約候生,再基于契約,對(duì)提供者驗(yàn)證的一種測(cè)試方式绽昼。
通常在開(kāi)發(fā)過(guò)程中主要是由服務(wù)提供方來(lái)提供約定接口唯鸭,雖然提供方架構(gòu)和接口進(jìn)行調(diào)整后會(huì)通知消費(fèi)者,但還是會(huì)存在風(fēng)險(xiǎn)的硅确。而在微服務(wù)架構(gòu)中目溉,不同的服務(wù)可能會(huì)由不同的團(tuán)隊(duì)維護(hù),這種情況下對(duì)接口的開(kāi)發(fā)和維護(hù)也將會(huì)帶來(lái)一些問(wèn)題菱农。
為了解決這些問(wèn)題呢缭付,Ian Robinson提出了一個(gè)以服務(wù)消費(fèi)者定義契約為驅(qū)動(dòng)的開(kāi)發(fā)模式:“Consumer-Driver Contracts(CDC)”,就是:消費(fèi)者驅(qū)動(dòng)契約循未。而 CDC 的總體流程是陷猫,消費(fèi)者定義了它們期望的 API/消息 是什么樣子,這種期望就被稱為契約的妖,提供者需要編寫驗(yàn)證這些契約并生成 stubs 供生產(chǎn)者重復(fù)使用绣檬。
Spring Cloud Contract 則是 CDC 的一種具體實(shí)現(xiàn)。
二 微服務(wù)架構(gòu)下測(cè)試存在的一些問(wèn)題
假設(shè)我們有一個(gè)由多個(gè) services 組成的系統(tǒng)嫂粟,我們?cè)跊](méi)有 CDC 的情況下對(duì) v1 這個(gè) service 進(jìn)行測(cè)試:
- 對(duì)于端到端的測(cè)試
- 為了測(cè)試一個(gè) service娇未,我們需要將所有的 services 和對(duì)應(yīng)的 databases 都部署起來(lái),一旦我們有很多的 service 后星虹,這個(gè)成本非常大
- 對(duì)于單元/集成測(cè)試
- 在編寫測(cè)試時(shí)候需要 mock 大量的數(shù)據(jù)
- 很難應(yīng)對(duì)服務(wù)端調(diào)整架構(gòu)或接口后帶來(lái)的問(wèn)題
三 Spring Cloud Contract 的解決方案
Spring Cloud Contract 可以為我們生成一個(gè)可被驗(yàn)證的 Stub Runner零抬,這樣我們就可以在不啟動(dòng)其它 services 的同時(shí)及時(shí)的獲取反饋信息镊讼,因?yàn)樵陔p方都準(zhǔn)守契約的情況下這個(gè) Stub Runner 就相當(dāng)于我們啟動(dòng)了對(duì)應(yīng)的 service
好處:
- 確保 WireMock/message stubs 是完全按照實(shí)際的服務(wù)端實(shí)現(xiàn)的
- 推廣 ATDD 方法和為服務(wù)架構(gòu)
- 當(dāng)提供者發(fā)布最新的契約時(shí),雙方都可以快速發(fā)現(xiàn)
- 既可以對(duì)自身服務(wù)進(jìn)行測(cè)試平夜,也可以生成 stub 提供給其它 service 進(jìn)行調(diào)用
四 Quickly Start
server side
Spring Cloud Contract 采用 Groovy DSL 來(lái)定義契約蝶棋,當(dāng)然了你也可以采用 yml 來(lái)編寫。現(xiàn)在我們來(lái)實(shí)現(xiàn)一個(gè)簡(jiǎn)單的 stub 褥芒,包含兩個(gè) API嚼松,根據(jù) id 獲取用戶信息以及添加一條用戶信息
1 編寫 contracts
add user
import org.springframework.cloud.contract.spec.Contract
Contract.make {
name 'should_return_status_created' //最終生成測(cè)試時(shí)的方法名,默認(rèn)是文件名
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' //最終生成測(cè)試時(shí)的方法名锰扶,默認(rèn)是文件名
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'))
當(dāng)生成 stub 后献酗,
-
consumer
表示客戶端在發(fā)送該請(qǐng)求時(shí)可以進(jìn)行正則匹配,只需要保證
users
后跟的是一個(gè)數(shù)字即可 -
producer
表示當(dāng)客服端發(fā)送了符合正則的請(qǐng)求后坷牛,所以符合的請(qǐng)求都將調(diào)用/users/1
這個(gè)接口
而對(duì)于 response 也是類似
response {
status 200
body([
id : 1L,
username: "acey",
])
testMatchers {
jsonPath('$.id', byRegex(number()))
jsonPath('$.username',byRegex(onlyAlphaUnicode()))
}
headers {
contentType(applicationJsonUtf8())
}
}
body
里面的數(shù)據(jù)是客戶端在請(qǐng)求后返回的具體的結(jié)果
而 testMatchers
則是針對(duì)服務(wù)端在自身測(cè)試時(shí)進(jìn)行一個(gè)模糊驗(yàn)證
2 生成/運(yùn)行測(cè)試
使用 ./gradlew generateContractTests
生成 contracts 對(duì)應(yīng)的測(cè)試罕偎,當(dāng)然你也可以直接 build / test 。生成的測(cè)試長(zhǎng)這樣
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}]*");
}
}
有必要再來(lái)看一下目錄結(jié)構(gòu)
3 生成 stub
執(zhí)行 ./gradlew install
會(huì)在本機(jī)的 .m2/repository
下生成一個(gè) **-stub.jar
當(dāng)然你也可以發(fā)布發(fā)布到一個(gè) remote repository
client side
當(dāng)有了 stub 之后京闰,在 client side 去調(diào)用就比較簡(jiǎn)單了颜及,在這就只演示下 get user
這個(gè) API (當(dāng)然你也可以采用契約測(cè)試進(jìn)行測(cè)試)
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
運(yùn)行的時(shí)候會(huì)在我們本地 `.m2/repository` 目錄下去找 com.acey:server:+:stubs 這個(gè)這個(gè) jar 文件
并 run 起來(lái),端口是 10001蹂楣,`+` 表示會(huì)自動(dòng)找最新的版本
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<Map> result = template.getForEntity(getUsersUrl, Map.class);
return result;
}
}
application-test.yml
userCenter: http://localhost:10001
五 獨(dú)立部署
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. 在同路徑創(chuàng)建配置文件application.yml俏站,并添加stubrunner的配置,如下
stubrunner:
ids:
# - [groupId]:artifactId:[version]:[classify]:[port]
# 指定運(yùn)行在8081端口
- com.jtj.cloud:spring-contract-example:1.0.0:+:8081
# 該模式下會(huì)從遠(yuǎn)程maven倉(cāng)庫(kù)緩存到本地
stubs-mode: local[remote 每次拉取遠(yuǎn)程]
3. 通過(guò)java -jar stub-runner.jar運(yùn)行痊土,訪問(wèn)對(duì)應(yīng)端口請(qǐng)求數(shù)據(jù)
參考: