diff --git a/docs/src/main/asciidoc/spring-cloud-contract.adoc b/docs/src/main/asciidoc/spring-cloud-contract.adoc index 88f7fac65c..d315b439af 100644 --- a/docs/src/main/asciidoc/spring-cloud-contract.adoc +++ b/docs/src/main/asciidoc/spring-cloud-contract.adoc @@ -8,7 +8,7 @@ Olga Maciaszek-Sharma, Mariusz Smykuła, Dave Syer_ == Spring Cloud Contract -What you always need it confidence in pushing new features into a new application or service in a distributed system. +What you always need is confidence in pushing new features into a new application or service in a distributed system. This project provides support for Consumer Driven Contracts and service schemas in Spring applications, covering a range of options for writing tests, publishing them as assets, asserting that a contract is kept by producers and consumers, for HTTP and message-based interactions. diff --git a/docs/src/main/asciidoc/spring-cloud-wiremock.adoc b/docs/src/main/asciidoc/spring-cloud-wiremock.adoc index 7287c0b742..9abc93d0fb 100644 --- a/docs/src/main/asciidoc/spring-cloud-wiremock.adoc +++ b/docs/src/main/asciidoc/spring-cloud-wiremock.adoc @@ -1,4 +1,88 @@ -Modules giving you the possibility to use http://wiremock.org[WireMock] with different servers. Check out the -https://github.com/spring-cloud/spring-cloud-contract/tree/master/samples[samples] for more information. +Modules giving you the possibility to use +http://wiremock.org[WireMock] with different servers by using the +"ambient" server embedded in a Spring Boot application. Check out the +https://github.com/spring-cloud/spring-cloud-contract/tree/master/samples[samples] +for more details. -Currently we support Jetty, Native WireMock server, Tomcat and Undertow. \ No newline at end of file +If you have a Spring Boot application that uses Tomcat as an embedded +server, for example (the default with `spring-boot-starter-web`), then +you can simply add `spring-cloud-contract-wiremock` to your classpath +and add `@AutoConfigureWireMock` in order to be able to use Wiremock +in your tests. Wiremock runs as a stub server and you can register +stub behaviour using a Java API or via static JSON declarations as +part of your test. Here's a simple example: + +[source,java,indent=0] +---- +@RunWith(SpringRunner.class) +@SpringBootTest +@AutoConfigureWireMock +public class WiremockImportApplicationTests { + + // A service that calls out over HTTP to localhost:8080 + @Autowired + private Service service; + + @Test + public void contextLoads() throws Exception { + // Using the WireMock APIs in the normal way: + stubFor(get(urlEqualTo("/resource")) + .willReturn(aResponse().withHeader("Content-Type", "text/plain").withBody("Hello World!"))); + assertThat(this.service.go()).isEqualTo("Hello World!"); + } + +} +---- + +To start the stub server on a different port use `@AutoConfigureWireMock(port=9999)` (for example), and for a random port use the value 0. The stub server port will be bindable in the test application context as "wiremock.server.port". Using `@AutoConfigureWireMock` adds a bean of type `WiremockConfiguration` to your test application context, where it will be cached in between methods and classes having the same context, just like for normal Spring integration tests. + +For a more conventional WireMock experience, using JUnit `@Rules` to +start and stop the server, just use the `WireMockSpring` convenience +class to obtain an `Options` instance: + +[source,java,indent=0] +---- + @ClassRule + public static WireMockClassRule wiremock = new WireMockClassRule( + WireMockSpring.options()); +---- + +The use `@ClassRule` means that the server will shut down after all the methods in this class. + +== WireMock and Spring MVC Mocks + +Spring Cloud Contract provides a convenience class that can load JSON WireMock stubs into a Spring `MockRestServiceServer`. Here's an example: + +[source,java,indent=0] +---- + @Autowired + private RestTemplate restTemplate; + + @Autowired + private Service service; + + @Test + public void contextLoads() throws Exception { + MockRestServiceServer server = WireMockExpectations.with(this.restTemplate) + .baseUrl("http://example.org") + .expect("resource"); + assertThat(this.service.go()).isEqualTo("Hello World"); + server.verify(); + } + +---- + +The `baseUrl` is prepended to all mock calls, and the `expect()` +method takes a stub name as an argument, where the stubs are stored in +the classpath at `/stubs/.json` by default. So in this example +the stub defined at `/stubs/resource.json` is loaded into the mock +server, so if the `RestTemplate` is asked to visit +`http://example.org/` it will get the responses as declared there. The +JSON format is the normal WireMock format which you can read about in +the WireMock website. + +Currently we support Tomcat, Jetty and Undertow as Spring Boot +embedded servers, and Wiremock itself has "native" support for a +particular version of Jetty (currently 9.2). To use the native Jetty +you need to add the native wiremock dependencies and exclude the +Spring Boot container if there is one. \ No newline at end of file diff --git a/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/AutoConfigureWireMock.java b/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/AutoConfigureWireMock.java index cc66959f8d..4b84a97e00 100644 --- a/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/AutoConfigureWireMock.java +++ b/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/AutoConfigureWireMock.java @@ -22,6 +22,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.springframework.boot.test.autoconfigure.properties.PropertyMapping; import org.springframework.context.annotation.Import; /** @@ -32,6 +33,9 @@ import org.springframework.context.annotation.Import; @Retention(RetentionPolicy.RUNTIME) @Documented @Import(WireMockConfiguration.class) +@PropertyMapping("wiremock.server") public @interface AutoConfigureWireMock { + + int port() default 8080; } diff --git a/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/SpringBootHttpServerFactory.java b/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/SpringBootHttpServerFactory.java index 9f1e342be5..78c5a41208 100644 --- a/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/SpringBootHttpServerFactory.java +++ b/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/SpringBootHttpServerFactory.java @@ -40,6 +40,7 @@ import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.autoconfigure.web.ServerPropertiesAutoConfiguration; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory; +import org.springframework.boot.context.embedded.EmbeddedWebApplicationContext; import org.springframework.boot.context.embedded.Ssl; import org.springframework.boot.context.embedded.jetty.JettyEmbeddedServletContainerFactory; import org.springframework.boot.context.embedded.jetty.JettyServerCustomizer; @@ -125,11 +126,16 @@ class SpringBootHttpServer @Override public int port() { - return this.options.portNumber(); + if (options.httpsSettings().enabled()) { + return options.portNumber(); + } + EmbeddedWebApplicationContext embedded = (EmbeddedWebApplicationContext) context; + return embedded.getEmbeddedServletContainer().getPort(); } @Override public int httpsPort() { + // TODO HTTPS on random port return this.options.httpsSettings().port(); } diff --git a/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/WireMockApplicationListener.java b/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/WireMockApplicationListener.java new file mode 100644 index 0000000000..3861c75325 --- /dev/null +++ b/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/WireMockApplicationListener.java @@ -0,0 +1,57 @@ +/* + * Copyright 2012-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.wiremock; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.boot.context.event.ApplicationPreparedEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.MutablePropertySources; +import org.springframework.util.SocketUtils; + +/** + * @author Dave Syer + * + */ +@Order(Ordered.LOWEST_PRECEDENCE) +public class WireMockApplicationListener implements ApplicationListener { + + @Override + public void onApplicationEvent(ApplicationPreparedEvent event) { + registerPort(event.getApplicationContext().getEnvironment()); + } + + private void registerPort(ConfigurableEnvironment environment) { + if (environment.getProperty("wiremock.server.port", Integer.class, 0) == 0) { + MutablePropertySources propertySources = environment.getPropertySources(); + if (!propertySources.contains("wiremock")) { + propertySources.addFirst( + new MapPropertySource("wiremock", new HashMap())); + } + Map source = ((MapPropertySource) propertySources + .get("wiremock")).getSource(); + source.put("wiremock.server.port", SocketUtils.findAvailableTcpPort(10000, 15000)); + } + } + + +} diff --git a/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/WireMockConfiguration.java b/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/WireMockConfiguration.java index 069bb3b513..460a9b700f 100644 --- a/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/WireMockConfiguration.java +++ b/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/WireMockConfiguration.java @@ -19,8 +19,12 @@ package org.springframework.cloud.contract.wiremock; import javax.annotation.PostConstruct; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.SmartLifecycle; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.ImportAware; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.type.AnnotationMetadata; import com.github.tomakehurst.wiremock.WireMockServer; import com.github.tomakehurst.wiremock.client.WireMock; @@ -31,7 +35,7 @@ import com.github.tomakehurst.wiremock.core.Options; * */ @Configuration -class WireMockConfiguration implements SmartLifecycle { +public class WireMockConfiguration implements SmartLifecycle, ImportAware { private volatile boolean running; @@ -39,13 +43,31 @@ class WireMockConfiguration implements SmartLifecycle { @Autowired(required = false) private Options options; + + @Value("${wiremock.server.port:8080}") + private int port = 8080; + + @Override + public void setImportMetadata(AnnotationMetadata metadata) { + int port = AnnotationAttributes + .fromMap(metadata + .getAnnotationAttributes(AutoConfigureWireMock.class.getName())) + .getNumber("port").intValue(); + if (port>0) { + this.port = port; + } + } @PostConstruct public void init() { if (options == null) { - this.options = com.github.tomakehurst.wiremock.core.WireMockConfiguration + com.github.tomakehurst.wiremock.core.WireMockConfiguration factory = com.github.tomakehurst.wiremock.core.WireMockConfiguration .wireMockConfig() .httpServerFactory(new SpringBootHttpServerFactory()); + if (port != 8080) { + factory.port(port); + } + this.options = factory; } server = new WireMockServer(options); } @@ -53,7 +75,7 @@ class WireMockConfiguration implements SmartLifecycle { @Override public void start() { server.start(); - WireMock.configureFor("localhost", options.portNumber()); + WireMock.configureFor("localhost", server.port()); running = true; } diff --git a/spring-cloud-contract-wiremock/src/main/resources/META-INF/spring.factories b/spring-cloud-contract-wiremock/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000000..fcabcba368 --- /dev/null +++ b/spring-cloud-contract-wiremock/src/main/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ +# Application Listeners +org.springframework.context.ApplicationListener=\ +org.springframework.cloud.contract.wiremock.WireMockApplicationListener \ No newline at end of file diff --git a/spring-cloud-contract-wiremock/src/test/java/org/springframework/cloud/contract/wiremock/AutoConfigureWireMockApplicationTests.java b/spring-cloud-contract-wiremock/src/test/java/org/springframework/cloud/contract/wiremock/AutoConfigureWireMockApplicationTests.java index 7775b0ff9a..f2460dfd74 100644 --- a/spring-cloud-contract-wiremock/src/test/java/org/springframework/cloud/contract/wiremock/AutoConfigureWireMockApplicationTests.java +++ b/spring-cloud-contract-wiremock/src/test/java/org/springframework/cloud/contract/wiremock/AutoConfigureWireMockApplicationTests.java @@ -15,7 +15,7 @@ import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit4.SpringRunner; @RunWith(SpringRunner.class) -@SpringBootTest(classes=WiremockTestsApplication.class, properties="app.baseUrl=http://localhost:8080", webEnvironment=WebEnvironment.NONE) +@SpringBootTest(classes=WiremockTestsApplication.class, properties="app.baseUrl=http://localhost:${wiremock.server.port}", webEnvironment=WebEnvironment.NONE) @DirtiesContext @AutoConfigureWireMock public class AutoConfigureWireMockApplicationTests { diff --git a/spring-cloud-contract-wiremock/src/test/java/org/springframework/cloud/contract/wiremock/AutoConfigureWireMockPortApplicationTests.java b/spring-cloud-contract-wiremock/src/test/java/org/springframework/cloud/contract/wiremock/AutoConfigureWireMockPortApplicationTests.java new file mode 100644 index 0000000000..9f18fe9a51 --- /dev/null +++ b/spring-cloud-contract-wiremock/src/test/java/org/springframework/cloud/contract/wiremock/AutoConfigureWireMockPortApplicationTests.java @@ -0,0 +1,33 @@ +package org.springframework.cloud.contract.wiremock; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringRunner; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = WiremockTestsApplication.class, properties = "app.baseUrl=http://localhost:${wiremock.server.port}", webEnvironment = WebEnvironment.NONE) +@DirtiesContext +@AutoConfigureWireMock(port = 9999) +public class AutoConfigureWireMockPortApplicationTests { + + @Autowired + private Service service; + + @Test + public void contextLoads() throws Exception { + stubFor(get(urlEqualTo("/resource")).willReturn(aResponse() + .withHeader("Content-Type", "text/plain").withBody("Hello World!"))); + assertThat(this.service.go()).isEqualTo("Hello World!"); + } + +} diff --git a/spring-cloud-contract-wiremock/src/test/java/org/springframework/cloud/contract/wiremock/AutoConfigureWireMockRandomPortApplicationTests.java b/spring-cloud-contract-wiremock/src/test/java/org/springframework/cloud/contract/wiremock/AutoConfigureWireMockRandomPortApplicationTests.java new file mode 100644 index 0000000000..0ca1c70e0e --- /dev/null +++ b/spring-cloud-contract-wiremock/src/test/java/org/springframework/cloud/contract/wiremock/AutoConfigureWireMockRandomPortApplicationTests.java @@ -0,0 +1,33 @@ +package org.springframework.cloud.contract.wiremock; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringRunner; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes=WiremockTestsApplication.class, properties="app.baseUrl=http://localhost:${wiremock.server.port}", webEnvironment=WebEnvironment.NONE) +@DirtiesContext +@AutoConfigureWireMock(port=0) +public class AutoConfigureWireMockRandomPortApplicationTests { + + @Autowired + private Service service; + + @Test + public void contextLoads() throws Exception { + stubFor(get(urlEqualTo("/resource")) + .willReturn(aResponse().withHeader("Content-Type", "text/plain").withBody("Hello World!"))); + assertThat(this.service.go()).isEqualTo("Hello World!"); + } + +}