使用 REST Docs
你可以使用 Spring REST Docs 透過 Spring MockMvc、WebTestClient 或 RestAssured 為 HTTP API 生成文件(例如,Asciidoc 格式)。在生成 API 文件的同時,你還可以使用 Spring Cloud Contract WireMock 生成 WireMock 存根。為此,編寫你常規的 REST Docs 測試用例,並使用 @AutoConfigureRestDocs
在 REST Docs 輸出目錄中自動生成存根。下面的 UML 圖顯示了 REST Docs 的流程

以下示例使用了 MockMvc
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureRestDocs(outputDir = "target/snippets")
@AutoConfigureMockMvc
public class ApplicationTests {
@Autowired
private MockMvc mockMvc;
@Test
public void contextLoads() throws Exception {
mockMvc.perform(get("/resource"))
.andExpect(content().string("Hello World"))
.andDo(document("resource"));
}
}
此測試會在 target/snippets/stubs/resource.json
生成一個 WireMock 存根。它匹配所有針對 /resource
路徑的 GET
請求。使用 WebTestClient(用於測試 Spring WebFlux 應用)的相同示例如下
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureRestDocs(outputDir = "target/snippets")
@AutoConfigureWebTestClient
public class ApplicationTests {
@Autowired
private WebTestClient client;
@Test
public void contextLoads() throws Exception {
client.get().uri("/resource").exchange()
.expectBody(String.class).isEqualTo("Hello World")
.consumeWith(document("resource"));
}
}
無需任何額外配置,這些測試會建立一個帶有請求匹配器的存根,用於匹配 HTTP 方法以及除 host
和 content-length
外的所有頭部。為了更精確地匹配請求(例如,匹配 POST 或 PUT 的請求體),我們需要顯式地建立一個請求匹配器。這樣做有兩個效果
-
建立一個僅按你指定方式匹配的存根。
-
斷言測試用例中的請求也匹配相同的條件。
此特性的主要入口點是 WireMockRestDocs.verify()
,它可以作為 document()
便利方法的替代,如下例所示
import static org.springframework.cloud.contract.wiremock.restdocs.WireMockRestDocs.verify;
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureRestDocs(outputDir = "target/snippets")
@AutoConfigureMockMvc
public class ApplicationTests {
@Autowired
private MockMvc mockMvc;
@Test
public void contextLoads() throws Exception {
mockMvc.perform(post("/resource")
.content("{\"id\":\"123456\",\"message\":\"Hello World\"}"))
.andExpect(status().isOk())
.andDo(verify().jsonPath("$.id"))
.andDo(document("resource"));
}
}
前面的契約指定了任何帶有 id
欄位的有效 POST 請求都會接收此測試中定義的響應。你可以連結呼叫 .jsonPath()
來新增更多匹配器。如果你不熟悉 JSON Path,JayWay 文件 可以幫助你快速入門。WebTestClient 版本的此測試有一個類似的 verify()
靜態幫助方法,你可以將其插入到相同位置。
除了 jsonPath
和 contentType
便利方法外,你還可以使用 WireMock API 來驗證請求是否匹配建立的存根,如下例所示
@Test
public void contextLoads() throws Exception {
mockMvc.perform(post("/resource")
.content("{\"id\":\"123456\",\"message\":\"Hello World\"}"))
.andExpect(status().isOk())
.andDo(verify()
.wiremock(WireMock.post(urlPathEquals("/resource"))
.withRequestBody(matchingJsonPath("$.id"))
.andDo(document("post-resource"))));
}
WireMock API 功能豐富。你可以透過正則表示式以及 JSON path 匹配頭部、查詢引數和請求體。你可以使用這些特性建立包含更廣泛引數的存根。前面的示例生成了一個類似於下面示例的存根
{
"request" : {
"url" : "/resource",
"method" : "POST",
"bodyPatterns" : [ {
"matchesJsonPath" : "$.id"
}]
},
"response" : {
"status" : 200,
"body" : "Hello World",
"headers" : {
"X-Application-Context" : "application:-1",
"Content-Type" : "text/plain"
}
}
}
你可以使用 wiremock() 方法或 jsonPath() 和 contentType() 方法來建立請求匹配器,但不能同時使用這兩種方法。 |
在消費者側,你可以將本節前面生成的 resource.json
檔案在類路徑中可用(例如,透過將存根釋出為 JAR)。之後,你可以透過多種不同方式建立一個使用 WireMock 的存根,包括使用 @AutoConfigureWireMock(stubs="classpath:resource.json")
,如本文件前面所述。
使用 REST Docs 生成契約
你還可以使用 Spring REST Docs 生成 Spring Cloud Contract DSL 檔案和文件。如果將其與 Spring Cloud WireMock 結合使用,你將同時獲得契約和存根。
為什麼會想要使用此功能?社群中的一些人問到了這樣的情況:他們希望轉向基於 DSL 的契約定義,但他們已經有很多 Spring MVC 測試。使用此功能可以讓你生成契約檔案,你以後可以修改並將它們移動到資料夾(在你的配置中定義),以便外掛能夠找到它們。
你可能想知道為什麼此功能在 WireMock 模組中。此功能在那裡是因為同時生成契約和存根是有意義的。 |
考慮以下測試
this.mockMvc
.perform(post("/foo").accept(MediaType.APPLICATION_PDF)
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"foo\": 23, \"bar\" : \"baz\" }"))
.andExpect(status().isOk())
.andExpect(content().string("bar"))
// first WireMock
.andDo(WireMockRestDocs.verify()
.jsonPath("$[?(@.foo >= 20)]")
.jsonPath("$[?(@.bar in ['baz','bazz','bazzz'])]")
.contentType(MediaType.valueOf("application/json")))
// then Contract DSL documentation
.andDo(document("index", SpringCloudContractRestDocs.dslContract(Maps.of("priority", 1))));
前面的測試建立了上一節中展示的存根,同時生成了契約和文件檔案。
契約檔名為 index.groovy
,可能類似於以下示例
import org.springframework.cloud.contract.spec.Contract
Contract.make {
request {
method 'POST'
url '/foo'
body('''
{"foo": 23 }
''')
headers {
header('''Accept''', '''application/json''')
header('''Content-Type''', '''application/json''')
}
}
response {
status OK()
body('''
bar
''')
headers {
header('''Content-Type''', '''application/json;charset=UTF-8''')
header('''Content-Length''', '''3''')
}
bodyMatchers {
jsonPath('$[?(@.foo >= 20)]', byType())
}
}
}
生成的文件(在本例中格式為 Asciidoc)包含格式化的契約。此檔案的位置將是 index/dsl-contract.adoc
。
指定 priority 屬性
方法 SpringCloudContractRestDocs.dslContract()
接受一個可選的 Map 引數,允許你在模板中指定附加屬性。
其中一個屬性是 priority 欄位,你可以按如下方式指定
SpringCloudContractRestDocs.dslContract(Map.of("priority", 1))
覆蓋 DSL 契約模板
預設情況下,契約的輸出基於一個名為 default-dsl-contract-only.snippet
的檔案。
你可以透過按如下方式覆蓋 getTemplate() 方法來提供自定義模板檔案
new ContractDslSnippet(){
@Override
protected String getTemplate() {
return "custom-dsl-contract";
}
}));
因此,上面顯示此行的示例
.andDo(document("index", SpringCloudContractRestDocs.dslContract()));
應更改為
.andDo(document("index", new ContractDslSnippet(){
@Override
protected String getTemplate() {
return "custom-dsl-template";
}
}));
模板透過查詢類路徑上的資源來解析。按以下順序檢查以下位置
-
org/springframework/restdocs/templates/${templateFormatId}/${name}.snippet
-
org/springframework/restdocs/templates/${name}.snippet
-
org/springframework/restdocs/templates/${templateFormatId}/default-${name}.snippet
因此,在上面的示例中,你應該將名為 custom-dsl-template.snippet 的檔案放在 src/test/resources/org/springframework/restdocs/templates/custom-dsl-template.snippet
路徑下