Spring 云合约:在 URL 生成的合约响应中访问主机名和端口
Spring Cloud Contract: Access hostname and port in contract response for URL generation
我们正在努力提供具有以下特征的合同:
import org.springframework.cloud.contract.spec.Contract
Contract.make {
request {
method(GET())
url("/v2/entity")
headers {
accept(applicationJson())
}
}
response {
status 200
body( """
{
"saveLink": "http://<requestedHost>:<requestedPort>/v2/entity/save"
}
)
}
}
如果我们的客户端使用 stubrunner 并选择不同的端口,e.q。 9876,"saveLink" 应在响应 URL 中反映此端口。
我们找不到一种简单的 API 方法来获取主机和端口信息。 fromRequest() 或 url() 仅 return 请求的相关部分 URL。这个需求有没有API的方法或者简单的解决办法?还有其他建议吗?
首先,我认为您误解了合同测试的概念。我想知道为什么在那里拥有这些具体价值观对你来说至关重要?在合同测试中,您只会对 link 包含 URL 和端口的一些值感兴趣。您甚至不会调用那个 URL。所以很可能你应该在继续之前改变你的方法。
如果您认为这是唯一的解决方法,我会就您如何解决这个问题提出我的意见(尽管我还没有测试过,但看起来应该可行;))。
目前要完成这一切并不容易。一旦这个 https://github.com/spring-cloud/spring-cloud-contract/pull/429 被合并,在即将发布的 Edgware 版本中会更容易。
不过我会想办法解决的。我们要做的是添加一个转换机制,该机制将在从 WireMock 发回之前修改响应负载。我们将需要转换 class 并且我们还需要扩展现有的存根服务机制。
首先,让我们创建一个自定义 WireMock 扩展,它将分析 WireMock 使用的每个映射。我们只想修改其中包含 saveLink
的那个。
class CustomExtension extends ResponseTransformer {
@Override
String getName() {
return "url-transformer";
}
/**
* Transformer that converts the save the way we want it to look like
*/
@Override
Response transform(Request request, Response response, FileSource files, Parameters parameters) {
if (requestRelatedToMyParticularCase(response)) {
String body = "\"{\"saveLink\" : \"http://"+ url.host + ":" + url.port + "/v2/entity/save\"}\"";
return new Response(response.getStatus(), response.getStatusMessage(),
body, response.getHeaders(), response.wasConfigured(), response.getFault(), response.isFromProxy());
}
// if it's not related continue as usual
return response;
}
private boolean requestRelatedToMyParticularCase(Response response) {
// is it related to your particular scenario ?
return response.bodyAsString.contains("saveLink");
}
/**
* We want to apply this transformation for all mappings
*/
@Override
boolean applyGlobally() {
return true
}
}
现在,您可以创建一个实现 HttpServerStub
的 class 并按此处所述进行注册 - http://cloud.spring.io/spring-cloud-static/Dalston.SR3/#_custom_stub_runner 。它基本上是 WireMockHttpServerStub
的副本,只是我们手动添加转换器
public class MyCustomWireMockHttpServerStub implements HttpServerStub {
private static final Logger log = LoggerFactory.getLogger(MyCustomWireMockHttpServerStub.class);
private static final int INVALID_PORT = -1;
private WireMockServer wireMockServer;
@Override
public HttpServerStub start(int port) {
this.wireMockServer = new WireMockServer(myConfig().port(port)
.notifier(new Slf4jNotifier(true)));
this.wireMockServer.start();
return this;
}
private WireMockConfiguration myConfig() {
if (ClassUtils.isPresent("org.springframework.cloud.contract.wiremock.WireMockSpring", null)) {
return WireMockSpring.options()
.extensions(responseTransformers());
}
return new WireMockConfiguration().extensions(responseTransformers());
}
private Extension[] responseTransformers() {
List<Extension> extensions = new ArrayList<>();
extensions.add(defaultResponseTemplateTransformer());
extensions.add(new CustomExtension());
return extensions.toArray(new Extension[extensions.size()]);
}
private ResponseTemplateTransformer defaultResponseTemplateTransformer() {
return new ResponseTemplateTransformer(false, helpers());
}
@Override
public int port() {
return isRunning() ? this.wireMockServer.port() : INVALID_PORT;
}
@Override
public boolean isRunning() {
return this.wireMockServer != null && this.wireMockServer.isRunning();
}
@Override
public HttpServerStub start() {
if (isRunning()) {
if (log.isDebugEnabled()) {
log.debug("The server is already running at port [" + port() + "]");
}
return this;
}
return start(SocketUtils.findAvailableTcpPort());
}
@Override
public HttpServerStub stop() {
if (!isRunning()) {
if (log.isDebugEnabled()) {
log.debug("Trying to stop a non started server!");
}
return this;
}
this.wireMockServer.stop();
return this;
}
@Override
public HttpServerStub registerMappings(Collection<File> stubFiles) {
if (!isRunning()) {
throw new IllegalStateException("Server not started!");
}
registerStubMappings(stubFiles);
return this;
}
@Override public String registeredMappings() {
Collection<String> mappings = new ArrayList<>();
for (StubMapping stubMapping : this.wireMockServer.getStubMappings()) {
mappings.add(stubMapping.toString());
}
return jsonArrayOfMappings(mappings);
}
private String jsonArrayOfMappings(Collection<String> mappings) {
return "[" + StringUtils.collectionToDelimitedString(mappings, ",\n") + "]";
}
@Override
public boolean isAccepted(File file) {
return file.getName().endsWith(".json");
}
StubMapping getMapping(File file) {
try (InputStream stream = Files.newInputStream(file.toPath())) {
return StubMapping.buildFrom(
StreamUtils.copyToString(stream, Charset.forName("UTF-8")));
}
catch (IOException e) {
throw new IllegalStateException("Cannot read file", e);
}
}
private void registerStubMappings(Collection<File> stubFiles) {
WireMock wireMock = new WireMock("localhost", port(), "");
registerDefaultHealthChecks(wireMock);
registerStubs(stubFiles, wireMock);
}
private void registerDefaultHealthChecks(WireMock wireMock) {
registerHealthCheck(wireMock, "/ping");
registerHealthCheck(wireMock, "/health");
}
private void registerStubs(Collection<File> sortedMappings, WireMock wireMock) {
for (File mappingDescriptor : sortedMappings) {
try {
wireMock.register(getMapping(mappingDescriptor));
if (log.isDebugEnabled()) {
log.debug("Registered stub mappings from [" + mappingDescriptor + "]");
}
}
catch (Exception e) {
if (log.isDebugEnabled()) {
log.debug("Failed to register the stub mapping [" + mappingDescriptor + "]", e);
}
}
}
}
private void registerHealthCheck(WireMock wireMock, String url) {
registerHealthCheck(wireMock, url, "OK");
}
private void registerHealthCheck(WireMock wireMock, String url, String body) {
wireMock.register(
WireMock.get(WireMock.urlEqualTo(url)).willReturn(WireMock.aResponse().withBody(body).withStatus(200)));
}
}
我们正在努力提供具有以下特征的合同:
import org.springframework.cloud.contract.spec.Contract
Contract.make {
request {
method(GET())
url("/v2/entity")
headers {
accept(applicationJson())
}
}
response {
status 200
body( """
{
"saveLink": "http://<requestedHost>:<requestedPort>/v2/entity/save"
}
)
}
}
如果我们的客户端使用 stubrunner 并选择不同的端口,e.q。 9876,"saveLink" 应在响应 URL 中反映此端口。 我们找不到一种简单的 API 方法来获取主机和端口信息。 fromRequest() 或 url() 仅 return 请求的相关部分 URL。这个需求有没有API的方法或者简单的解决办法?还有其他建议吗?
首先,我认为您误解了合同测试的概念。我想知道为什么在那里拥有这些具体价值观对你来说至关重要?在合同测试中,您只会对 link 包含 URL 和端口的一些值感兴趣。您甚至不会调用那个 URL。所以很可能你应该在继续之前改变你的方法。
如果您认为这是唯一的解决方法,我会就您如何解决这个问题提出我的意见(尽管我还没有测试过,但看起来应该可行;))。
目前要完成这一切并不容易。一旦这个 https://github.com/spring-cloud/spring-cloud-contract/pull/429 被合并,在即将发布的 Edgware 版本中会更容易。
不过我会想办法解决的。我们要做的是添加一个转换机制,该机制将在从 WireMock 发回之前修改响应负载。我们将需要转换 class 并且我们还需要扩展现有的存根服务机制。
首先,让我们创建一个自定义 WireMock 扩展,它将分析 WireMock 使用的每个映射。我们只想修改其中包含 saveLink
的那个。
class CustomExtension extends ResponseTransformer {
@Override
String getName() {
return "url-transformer";
}
/**
* Transformer that converts the save the way we want it to look like
*/
@Override
Response transform(Request request, Response response, FileSource files, Parameters parameters) {
if (requestRelatedToMyParticularCase(response)) {
String body = "\"{\"saveLink\" : \"http://"+ url.host + ":" + url.port + "/v2/entity/save\"}\"";
return new Response(response.getStatus(), response.getStatusMessage(),
body, response.getHeaders(), response.wasConfigured(), response.getFault(), response.isFromProxy());
}
// if it's not related continue as usual
return response;
}
private boolean requestRelatedToMyParticularCase(Response response) {
// is it related to your particular scenario ?
return response.bodyAsString.contains("saveLink");
}
/**
* We want to apply this transformation for all mappings
*/
@Override
boolean applyGlobally() {
return true
}
}
现在,您可以创建一个实现 HttpServerStub
的 class 并按此处所述进行注册 - http://cloud.spring.io/spring-cloud-static/Dalston.SR3/#_custom_stub_runner 。它基本上是 WireMockHttpServerStub
的副本,只是我们手动添加转换器
public class MyCustomWireMockHttpServerStub implements HttpServerStub {
private static final Logger log = LoggerFactory.getLogger(MyCustomWireMockHttpServerStub.class);
private static final int INVALID_PORT = -1;
private WireMockServer wireMockServer;
@Override
public HttpServerStub start(int port) {
this.wireMockServer = new WireMockServer(myConfig().port(port)
.notifier(new Slf4jNotifier(true)));
this.wireMockServer.start();
return this;
}
private WireMockConfiguration myConfig() {
if (ClassUtils.isPresent("org.springframework.cloud.contract.wiremock.WireMockSpring", null)) {
return WireMockSpring.options()
.extensions(responseTransformers());
}
return new WireMockConfiguration().extensions(responseTransformers());
}
private Extension[] responseTransformers() {
List<Extension> extensions = new ArrayList<>();
extensions.add(defaultResponseTemplateTransformer());
extensions.add(new CustomExtension());
return extensions.toArray(new Extension[extensions.size()]);
}
private ResponseTemplateTransformer defaultResponseTemplateTransformer() {
return new ResponseTemplateTransformer(false, helpers());
}
@Override
public int port() {
return isRunning() ? this.wireMockServer.port() : INVALID_PORT;
}
@Override
public boolean isRunning() {
return this.wireMockServer != null && this.wireMockServer.isRunning();
}
@Override
public HttpServerStub start() {
if (isRunning()) {
if (log.isDebugEnabled()) {
log.debug("The server is already running at port [" + port() + "]");
}
return this;
}
return start(SocketUtils.findAvailableTcpPort());
}
@Override
public HttpServerStub stop() {
if (!isRunning()) {
if (log.isDebugEnabled()) {
log.debug("Trying to stop a non started server!");
}
return this;
}
this.wireMockServer.stop();
return this;
}
@Override
public HttpServerStub registerMappings(Collection<File> stubFiles) {
if (!isRunning()) {
throw new IllegalStateException("Server not started!");
}
registerStubMappings(stubFiles);
return this;
}
@Override public String registeredMappings() {
Collection<String> mappings = new ArrayList<>();
for (StubMapping stubMapping : this.wireMockServer.getStubMappings()) {
mappings.add(stubMapping.toString());
}
return jsonArrayOfMappings(mappings);
}
private String jsonArrayOfMappings(Collection<String> mappings) {
return "[" + StringUtils.collectionToDelimitedString(mappings, ",\n") + "]";
}
@Override
public boolean isAccepted(File file) {
return file.getName().endsWith(".json");
}
StubMapping getMapping(File file) {
try (InputStream stream = Files.newInputStream(file.toPath())) {
return StubMapping.buildFrom(
StreamUtils.copyToString(stream, Charset.forName("UTF-8")));
}
catch (IOException e) {
throw new IllegalStateException("Cannot read file", e);
}
}
private void registerStubMappings(Collection<File> stubFiles) {
WireMock wireMock = new WireMock("localhost", port(), "");
registerDefaultHealthChecks(wireMock);
registerStubs(stubFiles, wireMock);
}
private void registerDefaultHealthChecks(WireMock wireMock) {
registerHealthCheck(wireMock, "/ping");
registerHealthCheck(wireMock, "/health");
}
private void registerStubs(Collection<File> sortedMappings, WireMock wireMock) {
for (File mappingDescriptor : sortedMappings) {
try {
wireMock.register(getMapping(mappingDescriptor));
if (log.isDebugEnabled()) {
log.debug("Registered stub mappings from [" + mappingDescriptor + "]");
}
}
catch (Exception e) {
if (log.isDebugEnabled()) {
log.debug("Failed to register the stub mapping [" + mappingDescriptor + "]", e);
}
}
}
}
private void registerHealthCheck(WireMock wireMock, String url) {
registerHealthCheck(wireMock, url, "OK");
}
private void registerHealthCheck(WireMock wireMock, String url, String body) {
wireMock.register(
WireMock.get(WireMock.urlEqualTo(url)).willReturn(WireMock.aResponse().withBody(body).withStatus(200)));
}
}