96. Spring Cloud Contract WireMock

Spring Cloud Contract WireMock 模块使您可以在 Spring Boot 应用程序中使用WireMock。请查看samples以获取更多详细信息。

如果您有一个使用 Tomcat 作为嵌入式服务器的 Spring Boot 应用程序(默认值为spring-boot-starter-web),则可以将spring-cloud-starter-contract-stub-runner添加到 Classpath 中,并添加@AutoConfigureWireMock以便能够在测试中使用 Wiremock。 Wiremock 作为存根服务器运行,您可以在测试中使用 Java API 或通过静态 JSON 声明来注册存根行为。以下代码显示了一个示例:

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

}

要在其他端口上启动存根服务器,请使用@AutoConfigureWireMock(port=9999)(例如)。对于随机端口,请使用0的值。可以在测试应用程序上下文中使用“ wiremock.server.port”属性绑定存根服务器端口。使用@AutoConfigureWireMock将类型WiremockConfiguration的 bean 添加到测试应用程序上下文中,该 bean 将被缓存在具有相同上下文的方法和类之间,这与 Spring 集成测试相同。您也可以将WireMockServer类型的 bean 注入测试中。

96.1 自动注册存根

如果使用@AutoConfigureWireMock,它将从文件系统或 Classpath(默认情况下为file:src/test/resources/mappings)注册 WireMock JSON 存根。您可以使用 Comments 中的stubs属性来自定义位置,该属性可以是 Ant 样式的资源模式或目录。如果是目录,则附加*/.json。以下代码显示了一个示例:

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

}

Note

实际上,WireMock 始终从 stubs 属性中的src/test/resources/mappings 以及 自定义位置加载 Map。要更改此行为,还可以按照本文档下一节中的说明指定文件根。

如果您使用的是 Spring Cloud Contract 的默认存根 jar,则您的存根将存储在/META-INF/group-id/artifact-id/versions/mappings/文件夹下。如果要从该位置,所有嵌入式 JAR 中注册所有存根,那么使用以下语法就足够了。

@AutoConfigureWireMock(port = 0, stubs = "classpath*:/META-INF/**/mappings/**/*.json")

96.2 使用文件指定存根实体

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

Note

当您配置files根目录时,它也会影响存根的自动加载,因为它们来自子目录“Map”中的根目录。 files的值对从stubs属性显式加载的存根没有影响。

96.3 替代方法:使用 JUnit 规则

要获得更常规的 WireMock 体验,可以使用 JUnit @Rules来启动和停止服务器。为此,请使用WireMockSpring便利类获取Options实例,如以下示例所示:

@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());

	@Before
	public void setup() {
		this.service.setBase("http://localhost:" + wiremock.port());
	}

	// A service that calls out over HTTP to wiremock's 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表示在运行了此类中的所有方法之后,服务器将关闭。

96.4 轻松 SSL 验证其余模板

WireMock 允许您使用“ https” URL 协议对“安全”服务器进行存根。如果您的应用程序希望在集成测试中联系该存根服务器,它将发现 SSL 证书无效(自安装证书的常见问题)。最好的选择通常是将 Client 端重新配置为使用“ http”。如果这不是一个选择,则可以要求 Spring 配置忽略 SSL 验证错误的 HTTPClient 端(当然,仅对测试而言如此)。

为了使此工作最小,您需要在应用程序中使用 Spring Boot RestTemplateBuilder,如以下示例所示:

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

您需要RestTemplateBuilder,因为构建器是通过回调传递的,以对其进行初始化,因此此时可以在 Client 端中设置 SSL 验证。如果您使用的是@AutoConfigureWireMock注解或存根运行程序,则会在测试中自动发生。如果使用 JUnit @Rule方法,则还需要添加@AutoConfigureHttpClientComments,如以下示例所示:

@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,则在 Classpath 上有 Apache HTTPClient 端,并且由RestTemplateBuilder选择它,并配置为忽略 SSL 错误。如果使用默认的java.netClient 端,则不需要 Comments(但不会造成任何危害)。当前不支持其他 Client 端,但可能会在将来的版本中添加。

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

96.5 WireMock 和 Spring MVC 模拟

Spring Cloud Contract 提供了一个便利类,可以将 JSON WireMock 存根加载到 Spring MockRestServiceServer中。以下代码显示了一个示例:

@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值位于所有模拟调用之前,并且stubs()方法采用存根路径资源模式作为参数。在前面的示例中,在/stubs/resource.json处定义的存根被加载到模拟服务器中。如果要求RestTemplate访问http://example.org/,则它将获得该 URL 处声明的响应。可以指定多个存根模式,每个存根模式都可以是目录(用于所有“ .json”的递归列表),固定文件名(如上例所示)或 Ant 样式的模式。 JSON 格式是标准的 WireMock 格式,您可以在WireMock website中进行阅读。

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

96.6 自定义 WireMock 配置

您可以注册org.springframework.cloud.contract.wiremock.WireMockConfigurationCustomizer类型的 bean,以自定义 WireMock 配置(例如,添加自定义转换器)。例:

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

96.7 使用 REST 文档生成存根

Spring REST 文件可用于为带有 Spring MockMvc 或WebTestClient或 Rest Assured 的 HTTP API 生成文档(例如,Asciidoctor 格式)。在为 API 生成文档的同时,还可以使用 Spring Cloud Contract WireMock 生成 WireMock 存根。为此,编写您的常规 REST Docs 测试用例,并使用@AutoConfigureRestDocs可以在 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 存根。它将所有 GET 请求与“/resource”路径匹配。带有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 方法的请求匹配器和所有 Headers(“主机”和“内容长度”除外)的存根。为了更精确地匹配请求(例如,匹配 POST 或 PUT 的正文),我们需要显式创建一个请求匹配器。这样做有两个效果:

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

  • assert 测试用例中的请求也匹配相同的条件。

此功能的主要入口点是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 都会收到此测试中定义的响应。您可以将对.jsonPath()的调用链接在一起,以添加其他匹配器。如果不熟悉 JSON 路径,则JayWay documentation可以帮助您快速Starter。此测试的WebTestClient版本具有与您在同一位置插入的类似verify()静态助手。

除了使用jsonPathcontentType便捷方法之外,您还可以使用 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"))
                       .stub("post-resource"));
}

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

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"
    }
  }
}

Note

您可以使用wiremock()方法或jsonPath()contentType()方法来创建请求匹配器,但是不能同时使用这两种方法。

在使用者方面,可以使本节前面生成的resource.json在 Classpath 上可用(例如,通过<< publishing-stubs-as-jars)。之后,可以使用 WireMock 以多种不同方式创建存根,包括使用@AutoConfigureWireMock(stubs="classpath:resource.json"),如本文档前面所述。

96.8 使用 REST 文档生成 Contract

您还可以使用 Spring REST Docs 生成 Spring Cloud Contract DSL 文件和文档。如果与 Spring Cloud WireMock 结合使用,则可以同时获取 Contract 和存根。

您为什么要使用此功能?社区中的一些人询问有关他们希望转向基于 DSL 的 Contract 定义的情况的问题,但是他们已经进行了许多 Spring MVC 测试。使用此功能,您可以生成 Contract 文件,以后可以修改 Contract 文件并将其移动到文件夹(在配置中定义),以便插件找到它们。

Tip

您可能想知道为什么 WireMock 模块中有此功能。之所以具有此功能,是因为生成 Contract 和存根都是有意义的。

考虑以下测试:

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,可能类似于以下示例:

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