98. Spring Cloud Contract WireMock

Spring Cloud Contract WireMock 模块允许您在 Spring Boot application 中使用WireMock。查看samples了解更多详情。

如果你有 Spring Boot application 使用 Tomcat 作为嵌入式服务器(默认为spring-boot-starter-web),你可以将spring-cloud-starter-contract-stub-runner添加到 classpath 并在 order 中添加@AutoConfigureWireMock以便能够在测试中使用 Wiremock。 Wiremock 作为存根服务器运行,您可以使用 Java API 或静态 JSON 声明来注册存根行为,作为测试的一部分。以下 code 显示了一个 example:

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@AutoConfigureWireMock(port = 0)
public class WiremockForDocsTests {
	// A service that calls out over HTTP
	@Autowired private Service service;

	// Using the WireMock APIs in the normal way:
	@Test
	public void contextLoads() throws Exception {
		// Stubbing WireMock
		stubFor(get(urlEqualTo("/resource"))
				.willReturn(aResponse().withHeader("Content-Type", "text/plain").withBody("Hello World!")));
		// We're asserting if WireMock responded properly
		assertThat(this.service.go()).isEqualTo("Hello World!");
	}

}

要在不同的 port 上启动存根服务器(对于 example),@AutoConfigureWireMock(port=9999)。对于随机 port,请使用_val的 value。可以使用“wiremock.server.port”property 在 test application context 中绑定存根服务器 port。使用@AutoConfigureWireMock将类型的 bean 添加到 test application context 中,它将被缓存在方法和具有相同 context 的 classes 之间,与 Spring integration 测试相同。

98.1 自动注册存根

如果使用@AutoConfigureWireMock,它会从文件系统或 classpath(默认情况下,从file:src/test/resources/mappings)注册 WireMock JSON 存根。您可以使用 annotation 中的stubs属性自定义位置,该属性可以是 Ant-style 资源 pattern 或目录。如果是目录,则追加*/.json。以下 code 显示了一个 example:

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureWireMock(stubs="classpath:/stubs")
public class WiremockImportApplicationTests {

	@Autowired
	private Service service;

	@Test
	public void contextLoads() throws Exception {
		assertThat(this.service.go()).isEqualTo("Hello World!");
	}

}

实际上,WireMock 总是加载来自src/test/resources/mappings **的映射以及来自 stubs 属性中的自定义位置。要更改此行为,您还可以指定 files root,如本文档的下一部分所述。

98.2 使用 Files 指定存根体

WireMock 可以从 classpath 或文件系统上的 files 读取响应主体。在这种情况下,您可以在 JSON DSL 中看到响应具有bodyFileName而不是(文字)body。 files 是相对于根目录解析的(默认情况下为src/test/resources/__files)。要自定义此位置,可以将@AutoConfigureWireMock annotation 中的files属性设置为 parent 目录的位置(换句话说,__files是子目录)。您可以使用 Spring 资源表示法来引用file:…classpath:…位置。不支持通用 URL。可以给出值列表,在这种情况下,WireMock 会在需要查找响应主体时解析存在的第一个文件。

配置files根时,它还会影响存根的自动加载,因为它们来自名为“mappings”的子目录中的根位置。 files的 value 对从stubs属性显式加载的存根没有影响。

98.3 备选:使用 JUnit 规则

对于更传统的 WireMock 体验,您可以使用 JUnit @Rules来启动和停止服务器。为此,请使用WireMockSpring convenience class 获取Options实例,如以下 example 所示:

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class WiremockForDocsClassRuleTests {

	// Start WireMock on some dynamic port
	// for some reason `dynamicPort()` is not working properly
	@ClassRule
	public static WireMockClassRule wiremock = new WireMockClassRule(
			WireMockSpring.options().dynamicPort());
	// A service that calls out over HTTP to localhost:${wiremock.port}
	@Autowired
	private Service service;

	// Using the WireMock APIs in the normal way:
	@Test
	public void contextLoads() throws Exception {
		// Stubbing WireMock
		wiremock.stubFor(get(urlEqualTo("/resource"))
				.willReturn(aResponse().withHeader("Content-Type", "text/plain").withBody("Hello World!")));
		// We're asserting if WireMock responded properly
		assertThat(this.service.go()).isEqualTo("Hello World!");
	}

}

@ClassRule表示服务器在此 class 中的所有方法都已运行后关闭。

98.4 Rest Template 的轻松 SSL 验证

WireMock 允许您使用“https”URL 协议存根“安全”服务器。如果您的 application 想要在 integration 测试中联系该存根服务器,它将发现 SSL 证书无效(self-installed 证书的常见问题)。最好的选择通常是 re-configure client 使用“http”。如果这不是一个选项,您可以要求 Spring 配置一个忽略 SSL 验证错误的 HTTP client(当然,仅对测试这样做)。

为了最大限度地减少这项工作,您需要在应用程序中使用 Spring Boot RestTemplateBuilder,如下面的示例所示:

@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
	return builder.build();
}

您需要RestTemplateBuilder,因为构建器通过回调来初始化它,因此可以在 client 中设置 SSL 验证。如果您使用@AutoConfigureWireMock annotation 或存根运行器,则会在测试中自动执行此操作。如果使用 JUnit @Rule方法,则还需要添加@AutoConfigureHttpClient annotation,如下面的 example 所示:

@RunWith(SpringRunner.class)
@SpringBootTest("app.baseUrl=https://localhost:6443")
@AutoConfigureHttpClient
public class WiremockHttpsServerApplicationTests {

	@ClassRule
	public static WireMockClassRule wiremock = new WireMockClassRule(
			WireMockSpring.options().httpsPort(6443));
...
}

如果您使用spring-boot-starter-test,则 class 路径上有 Apache HTTP client,它由RestTemplateBuilder选中并配置为忽略 SSL 错误。如果使用默认的java.net client,则不需要 annotation(但不会造成任何伤害)。目前没有其他客户端的支持,但可能会在将来的版本中添加。

要禁用自定义RestTemplateBuilder,请将wiremock.rest-template-ssl-enabled property 设置为false

98.5 WireMock 和 Spring MVC Mocks

Spring Cloud Contract 提供了一个方便的 class,它可以将 JSON WireMock 存根加载到 Spring MockRestServiceServer中。以下 code 显示了一个 example:

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.NONE)
public class WiremockForDocsMockServerApplicationTests {

	@Autowired
	private RestTemplate restTemplate;

	@Autowired
	private Service service;

	@Test
	public void contextLoads() throws Exception {
		// will read stubs classpath
		MockRestServiceServer server = WireMockRestServiceServer.with(this.restTemplate)
				.baseUrl("http://example.org").stubs("classpath:/stubs/resource.json")
				.build();
		// We're asserting if WireMock responded properly
		assertThat(this.service.go()).isEqualTo("Hello World");
		server.verify();
	}
}

baseUrl value 前置于所有 mock calls,stubs()方法将 stub 路径资源 pattern 作为参数。在前面的 example 中,/stubs/resource.json中定义的存根被加载到 mock 服务器中。如果要求RestTemplate访问http://example.org/,则会在该 URL 处声明响应。可以指定多个 stub pattern,每个都可以是一个目录(对于所有“.json”的递归列表),一个固定的文件名(如上面的 example 中所示)或 Ant-style pattern。 JSON 格式是普通的 WireMock 格式,您可以在WireMock 网站中阅读。

目前,Spring Cloud Contract Verifier 支持 Tomcat,Jetty 和 Undertow 作为 Spring Boot 嵌入式服务器,而 Wiremock 本身对 Jetty(当前 9.2)的特定 version 具有“本机”支持。要使用本机 Jetty,您需要添加本机 Wiremock 依赖项并排除 Spring Boot 容器(如果有)。

98.6 自定义 WireMock configuration

您可以在 order 中注册org.springframework.cloud.contract.wiremock.WireMockConfigurationCustomizer类型的 bean 来自定义 WireMock configuration(e.g. 添加自定义变换器)。 例:

@Bean WireMockConfigurationCustomizer optionsCustomizer() {
			return new WireMockConfigurationCustomizer() {
				@Override public void customize(WireMockConfiguration options) {
// perform your customization here
				}
			};
		}

98.7 使用 REST Docs 生成存根

Spring REST Docs可用于生成具有 Spring MockMvc 或WebTestClient或 Rest Assured 的 HTTP API 的文档(对于 Asciidoctor 格式的 example)。在为 API 生成文档的同一时间,您还可以使用 Spring Cloud Contract WireMock 生成 WireMock 存根。为此,编写正常的 REST Docs 测试用例并使用@AutoConfigureRestDocs在 REST Docs 输出目录中自动生成存根。以下 code 显示使用MockMvc的 example:

@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 存根。它将所有 GET 请求与“/resource”路径匹配。与WebTestClient(用于测试 Spring WebFlux applications)相同的 example 将如下所示:

@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"));
	}
}

在没有任何额外的 configuration 的情况下,这些测试会创建一个存根,其中包含 HTTP 方法的请求匹配器和除“host”和“content-length”之外的所有_header。要更精确地匹配请求(对于 example,要匹配 POST 或 PUT 的主体),我们需要显式创建请求匹配器。这样做有两个影响:

  • 创建仅以您指定的方式匹配的存根。

  • 断言测试用例中的请求也符合相同的条件。

此 feature 的主要入口点是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")
                        .stub("resource"));
	}
}

此 contract 指定具有“id”字段的任何有效 POST 都接收此测试中定义的响应。您可以将 calls 链接到.jsonPath()以添加其他匹配器。如果 JSON Path 不熟悉,JayWay 文档可以帮助您加快速度。这个测试的WebTestClient version 有一个类似的verify()静态帮助器,你可以在同一个地方插入它。

您还可以使用 WireMock API 来验证请求是否与创建的存根匹配,而不是jsonPathcontentType便捷方法,如以下 example 所示:

@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"))
                       .stub("post-resource"));
}

WireMock API 非常丰富。您可以通过正则表达式和 JSON 路径匹配 headers,查询参数和请求正文。这些 features 可用于创建具有更广泛参数的存根。上面的 example 生成一个类似于以下示例的存根:

post-resource.json.

{
  "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()方法来创建请求匹配器,但不能同时使用这两种方法。

在 consumer 端,您可以在 classpath 中使用本节前面生成的resource.json(对于 example,使用<< publishing-stubs-as-jars]。之后,您可以使用 WireMock 以多种不同方式创建存根,包括使用@AutoConfigureWireMock(stubs="classpath:resource.json"),如本文档前面所述。

98.8 使用 REST Docs 生成 Contracts

您还可以使用 Spring REST Docs 生成 Spring Cloud Contract DSL files 和文档。如果你与 Spring Cloud WireMock 结合使用,你会得到 contracts 和 stubs。

你为什么要使用这个 feature?社区中的一些人询问了他们想要转向 DSL-based contract 定义的情况,但他们已经有很多 Spring MVC 测试。使用此 feature 可以生成 contract files,稍后您可以修改这些文件并移动到文件夹(在 configuration 中定义),以便插件找到它们。

您可能想知道为什么这个功能在 WireMock 模块中。功能就在那里,因为生成 contracts 和 stubs 都是有意义的。

考虑以下测试:

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"))
						.stub("shouldGrantABeerIfOldEnough"))
				// then Contract DSL documentation
				.andDo(document("index", SpringCloudContractRestDocs.dslContract()));

上述测试创建了上一节中显示的存根,生成了 contract 和文档文件。

contract 被称为index.groovy,可能类似于以下 example:

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''')
        }
        testMatchers {
            jsonPath('$[?(@.foo >= 20)]', byType())
        }
    }
}

生成的文档(在本例中为 Asciidoc 格式)包含格式化的 contract。该文件的位置为index/dsl-contract.adoc