├── spring-boot-wiremock ├── src │ ├── test │ │ ├── resources │ │ │ ├── __files │ │ │ │ └── bodyFile.txt │ │ │ └── application.properties │ │ └── java │ │ │ └── de │ │ │ └── skuzzle │ │ │ └── springboot │ │ │ └── test │ │ │ └── wiremock │ │ │ ├── TestSpringBootApplication.java │ │ │ ├── WiremockContextCustomizerTest.java │ │ │ ├── WiremockAnnotationConfigurationTest.java │ │ │ ├── TestStubCollectionInterface.java │ │ │ ├── TestStubCollectionAnnotation.java │ │ │ ├── TestKeystoresTest.java │ │ │ ├── metaannotations │ │ │ ├── MetaAnnotatedTest.java │ │ │ └── WithSampleServiceMock.java │ │ │ ├── WireMockInitializerHttpTest.java │ │ │ ├── WireMockInitializerHttpsTest.java │ │ │ ├── HelloWorldTest.java │ │ │ ├── NestedTestTest.java │ │ │ ├── client │ │ │ └── TestClients.java │ │ │ └── TestHttpStub.java │ └── main │ │ ├── resources │ │ ├── certs │ │ │ ├── client_truststore.jks │ │ │ ├── server_keystore.jks │ │ │ ├── server_truststore.jks │ │ │ └── client_keystore.pkcs12 │ │ └── META-INF │ │ │ └── spring.factories │ │ └── java │ │ └── de │ │ └── skuzzle │ │ └── springboot │ │ └── test │ │ └── wiremock │ │ ├── stubs │ │ ├── Scenario.java │ │ ├── HttpStubs.java │ │ ├── Auth.java │ │ ├── WrapAround.java │ │ ├── Response.java │ │ ├── HttpStub.java │ │ └── Request.java │ │ ├── WiremockContextCustomizerFactory.java │ │ ├── StringValuePatterns.java │ │ ├── TestKeystores.java │ │ ├── WiremockContextCustomizer.java │ │ ├── WithWiremock.java │ │ ├── WiremockAnnotationConfiguration.java │ │ └── StubTranslator.java ├── ssl │ ├── client-cert.conf │ ├── ca-cert.conf │ ├── create-ca-keystore.sh │ └── create-client-cert.sh └── pom.xml ├── examples ├── src │ ├── main │ │ └── java │ │ │ └── de │ │ │ └── skuzzle │ │ │ └── springboot │ │ │ └── test │ │ │ └── example │ │ │ ├── ExampleSpringBootApplication.java │ │ │ ├── ApiClient.java │ │ │ ├── ApiClientProperties.java │ │ │ └── ApiClientConfiguration.java │ └── test │ │ └── java │ │ └── de │ │ └── skuzzle │ │ └── springboot │ │ └── test │ │ └── example │ │ └── ApiClientTest.java └── pom.xml ├── .gitignore ├── RELEASE_NOTES.md ├── readme ├── RELEASE_NOTES.md └── README.md ├── LICENSE ├── JenkinsfileJdkTests ├── Jenkinsfile ├── CHANGELOG_LEGACY.md ├── JenkinsfileRelease ├── pom.xml └── README.md /spring-boot-wiremock/src/test/resources/__files/bodyFile.txt: -------------------------------------------------------------------------------- 1 | Hello World -------------------------------------------------------------------------------- /spring-boot-wiremock/src/test/resources/application.properties: -------------------------------------------------------------------------------- 1 | logging.level.de.skuzzle.springboot.test.wiremock=debug 2 | url.in.propertiesfile=http://does.very.likely.not.exist:1337 -------------------------------------------------------------------------------- /spring-boot-wiremock/src/main/resources/certs/client_truststore.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skuzzle/spring-boot-wiremock/HEAD/spring-boot-wiremock/src/main/resources/certs/client_truststore.jks -------------------------------------------------------------------------------- /spring-boot-wiremock/src/main/resources/certs/server_keystore.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skuzzle/spring-boot-wiremock/HEAD/spring-boot-wiremock/src/main/resources/certs/server_keystore.jks -------------------------------------------------------------------------------- /spring-boot-wiremock/src/main/resources/certs/server_truststore.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skuzzle/spring-boot-wiremock/HEAD/spring-boot-wiremock/src/main/resources/certs/server_truststore.jks -------------------------------------------------------------------------------- /spring-boot-wiremock/src/main/resources/META-INF/spring.factories: -------------------------------------------------------------------------------- 1 | org.springframework.test.context.ContextCustomizerFactory = \ 2 | de.skuzzle.springboot.test.wiremock.WiremockContextCustomizerFactory -------------------------------------------------------------------------------- /spring-boot-wiremock/src/main/resources/certs/client_keystore.pkcs12: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skuzzle/spring-boot-wiremock/HEAD/spring-boot-wiremock/src/main/resources/certs/client_keystore.pkcs12 -------------------------------------------------------------------------------- /examples/src/main/java/de/skuzzle/springboot/test/example/ExampleSpringBootApplication.java: -------------------------------------------------------------------------------- 1 | package de.skuzzle.springboot.test.example; 2 | 3 | import org.springframework.boot.autoconfigure.SpringBootApplication; 4 | 5 | @SpringBootApplication 6 | public class ExampleSpringBootApplication { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /spring-boot-wiremock/src/test/java/de/skuzzle/springboot/test/wiremock/TestSpringBootApplication.java: -------------------------------------------------------------------------------- 1 | package de.skuzzle.springboot.test.wiremock; 2 | 3 | import org.springframework.boot.autoconfigure.SpringBootApplication; 4 | 5 | @SpringBootApplication 6 | public class TestSpringBootApplication { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | 3 | # Mobile Tools for Java (J2ME) 4 | .mtj.tmp/ 5 | 6 | # Package Files # 7 | *.jar 8 | *.war 9 | *.ear 10 | 11 | *.prefs 12 | .classpath 13 | .project 14 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 15 | hs_err_pid* 16 | target 17 | 18 | # IntelliJ 19 | .idea/ 20 | *.iml 21 | -------------------------------------------------------------------------------- /spring-boot-wiremock/src/test/java/de/skuzzle/springboot/test/wiremock/WiremockContextCustomizerTest.java: -------------------------------------------------------------------------------- 1 | package de.skuzzle.springboot.test.wiremock; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import nl.jqno.equalsverifier.EqualsVerifier; 6 | 7 | public class WiremockContextCustomizerTest { 8 | 9 | @Test 10 | void testEquals() throws Exception { 11 | EqualsVerifier.forClass(WiremockContextCustomizer.class).verify(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /spring-boot-wiremock/src/test/java/de/skuzzle/springboot/test/wiremock/WiremockAnnotationConfigurationTest.java: -------------------------------------------------------------------------------- 1 | package de.skuzzle.springboot.test.wiremock; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import nl.jqno.equalsverifier.EqualsVerifier; 6 | 7 | public class WiremockAnnotationConfigurationTest { 8 | @Test 9 | void testEquals() throws Exception { 10 | EqualsVerifier.forClass(WiremockAnnotationConfiguration.class).verify(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /spring-boot-wiremock/src/test/java/de/skuzzle/springboot/test/wiremock/TestStubCollectionInterface.java: -------------------------------------------------------------------------------- 1 | package de.skuzzle.springboot.test.wiremock; 2 | 3 | import de.skuzzle.springboot.test.wiremock.stubs.HttpStub; 4 | import de.skuzzle.springboot.test.wiremock.stubs.Request; 5 | 6 | @HttpStub(onRequest = @Request(toUrl = "/fromInterfaceCollection1")) 7 | @HttpStub(onRequest = @Request(toUrl = "/fromInterfaceCollection2")) 8 | public interface TestStubCollectionInterface { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /examples/src/main/java/de/skuzzle/springboot/test/example/ApiClient.java: -------------------------------------------------------------------------------- 1 | package de.skuzzle.springboot.test.example; 2 | 3 | import org.springframework.web.client.RestTemplate; 4 | 5 | public class ApiClient { 6 | 7 | private final RestTemplate resttemplate; 8 | 9 | public ApiClient(RestTemplate resttemplate) { 10 | this.resttemplate = resttemplate; 11 | } 12 | 13 | public String getResultsFromBackend() { 14 | return resttemplate.getForObject("/", String.class); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /spring-boot-wiremock/ssl/client-cert.conf: -------------------------------------------------------------------------------- 1 | [CA_default] 2 | copy_extensions = copy 3 | 4 | [req] 5 | default_bits = 4096 6 | prompt = no 7 | default_md = sha256 8 | distinguished_name = req_distinguished_name 9 | x509_extensions = v3_ca 10 | 11 | [req_distinguished_name] 12 | C = DE 13 | ST = Bramstedt 14 | O = skuzzle 15 | emailAddress = simon@taddiken.online 16 | CN = localhost 17 | 18 | [v3_ca] 19 | basicConstraints = CA:FALSE 20 | keyUsage = digitalSignature, keyEncipherment 21 | subjectAltName = @alternate_names 22 | 23 | [alternate_names] 24 | DNS.1 = localhost 25 | IP.1 = 127.0.0.1 26 | -------------------------------------------------------------------------------- /spring-boot-wiremock/ssl/ca-cert.conf: -------------------------------------------------------------------------------- 1 | [req] 2 | default_bits = 4096 3 | prompt = no 4 | default_md = sha256 5 | distinguished_name = req_distinguished_name 6 | x509_extensions = v3_ca 7 | default_days = 36525 8 | 9 | [req_distinguished_name] 10 | C = DE 11 | ST = Bramstedt 12 | O = skuzzle 13 | CN = localhost 14 | 15 | [v3_ca] 16 | subjectKeyIdentifier = hash 17 | authorityKeyIdentifier = keyid:always 18 | basicConstraints = critical, CA:TRUE 19 | keyUsage = critical, keyCertSign, cRLSign 20 | subjectAltName = @alternate_names 21 | 22 | [alternate_names] 23 | DNS.1 = localhost 24 | IP.1 = 127.0.0.1 -------------------------------------------------------------------------------- /spring-boot-wiremock/src/main/java/de/skuzzle/springboot/test/wiremock/stubs/Scenario.java: -------------------------------------------------------------------------------- 1 | package de.skuzzle.springboot.test.wiremock.stubs; 2 | 3 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 4 | 5 | import java.lang.annotation.Retention; 6 | 7 | import org.apiguardian.api.API; 8 | import org.apiguardian.api.API.Status; 9 | 10 | @API(status = Status.EXPERIMENTAL) 11 | @Retention(RUNTIME) 12 | public @interface Scenario { 13 | String name() default ""; 14 | 15 | String state() default com.github.tomakehurst.wiremock.stubbing.Scenario.STARTED; 16 | 17 | String nextState() default ""; 18 | } 19 | -------------------------------------------------------------------------------- /spring-boot-wiremock/src/test/java/de/skuzzle/springboot/test/wiremock/TestStubCollectionAnnotation.java: -------------------------------------------------------------------------------- 1 | package de.skuzzle.springboot.test.wiremock; 2 | 3 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 4 | 5 | import java.lang.annotation.ElementType; 6 | import java.lang.annotation.Retention; 7 | import java.lang.annotation.Target; 8 | 9 | import de.skuzzle.springboot.test.wiremock.stubs.HttpStub; 10 | import de.skuzzle.springboot.test.wiremock.stubs.Request; 11 | 12 | @Retention(RUNTIME) 13 | @Target({ ElementType.METHOD, ElementType.TYPE }) 14 | @HttpStub(onRequest = @Request(toUrl = "/fromAnnotationCollection1")) 15 | @HttpStub(onRequest = @Request(toUrl = "/fromAnnotationCollection2")) 16 | public @interface TestStubCollectionAnnotation { 17 | 18 | } 19 | -------------------------------------------------------------------------------- /spring-boot-wiremock/ssl/create-ca-keystore.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | read -r -s -p "Please enter a password for the key & keystore (default: password):" PASSWORD 5 | PASSWORD=${PASSWORD:=password} 6 | openssl req -x509 -newkey rsa:2048 -utf8 -days 365000 -nodes -config ca-cert.conf -keyout ca-cert.key -out ca-cert.crt 7 | openssl pkcs12 -export -inkey ca-cert.key -in ca-cert.crt -out ca-cert.p12 -password "pass:$PASSWORD" 8 | keytool -importkeystore -deststorepass "$PASSWORD" -destkeypass "$PASSWORD" -srckeystore ca-cert.p12 -srcstorepass "$PASSWORD" -deststoretype jks -destkeystore server_keystore.jks 9 | keytool -import -v -trustcacerts -alias 1 -file ca-cert.crt -keystore client_truststore.jks -keypass "$PASSWORD" -storepass "$PASSWORD" -noprompt 10 | rm ca-cert.key ca-cert.p12 ca-cert.crt -------------------------------------------------------------------------------- /spring-boot-wiremock/src/main/java/de/skuzzle/springboot/test/wiremock/stubs/HttpStubs.java: -------------------------------------------------------------------------------- 1 | package de.skuzzle.springboot.test.wiremock.stubs; 2 | 3 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 4 | 5 | import java.lang.annotation.ElementType; 6 | import java.lang.annotation.Retention; 7 | import java.lang.annotation.Target; 8 | 9 | import org.apiguardian.api.API; 10 | import org.apiguardian.api.API.Status; 11 | 12 | /** 13 | * Container annotation for repeatable {@link HttpStub} annotation. 14 | * 15 | * @author Simon Taddiken 16 | * @see HttpStub 17 | */ 18 | @API(status = Status.EXPERIMENTAL) 19 | @Retention(RUNTIME) 20 | @Target({ ElementType.METHOD, ElementType.TYPE }) 21 | public @interface HttpStubs { 22 | /** All the stubs that should be added. */ 23 | HttpStub[] value() default {}; 24 | } 25 | -------------------------------------------------------------------------------- /RELEASE_NOTES.md: -------------------------------------------------------------------------------- 1 | [![Maven Central](https://img.shields.io/static/v1?label=MavenCentral&message=0.0.18&color=blue)](https://search.maven.org/artifact/de.skuzzle.springboot.test/spring-boot-wiremock/0.0.18/jar) [![JavaDoc](https://img.shields.io/static/v1?label=JavaDoc&message=0.0.18&color=orange)](http://www.javadoc.io/doc/de.skuzzle.springboot.test/spring-boot-wiremock/0.0.18) 2 | 3 | ### Bug Fixes: 4 | * Fixed release process 5 | TEst 6 | 7 | 8 | ### Maven Central coordinates for this release 9 | 10 | ```xml 11 | 12 | de.skuzzle.springboot.test 13 | spring-boot-wiremock 14 | 0.0.18 15 | test 16 | 17 | ``` 18 | 19 | ### Gradle coordinates for this release 20 | 21 | ``` 22 | testImplementation 'de.skuzzle.springboot.test:spring-boot-wiremock:0.0.18' 23 | ``` -------------------------------------------------------------------------------- /spring-boot-wiremock/ssl/create-client-cert.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | read -r -s -p "Please enter a password for the key & keystore (default: password):" PASSWORD 5 | PASSWORD=${PASSWORD:=password} 6 | openssl req -x509 -newkey rsa:2048 -utf8 -days 365000 -nodes -config client-cert.conf -keyout client-cert.key -out client-cert.crt 7 | openssl pkcs12 -export -inkey client-cert.key -in client-cert.crt -out client-cert.p12 -password "pass:$PASSWORD" 8 | keytool -importkeystore -deststorepass "$PASSWORD" -destkeypass "$PASSWORD" -srckeystore client-cert.p12 -srcstorepass "$PASSWORD" -deststoretype pkcs12 -destkeystore client_keystore.pkcs12 9 | keytool -import -v -trustcacerts -alias server-cert -file client-cert.crt -keystore server_truststore.jks -keypass "$PASSWORD" -storepass "$PASSWORD" -noprompt 10 | rm client-cert.crt client-cert.key client-cert.p12 -------------------------------------------------------------------------------- /spring-boot-wiremock/src/test/java/de/skuzzle/springboot/test/wiremock/TestKeystoresTest.java: -------------------------------------------------------------------------------- 1 | package de.skuzzle.springboot.test.wiremock; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | public class TestKeystoresTest { 6 | 7 | @Test 8 | void testServerCertificateKeyStoreExists() throws Exception { 9 | TestKeystores.TEST_SERVER_CERTIFICATE.getKeystore(); 10 | } 11 | 12 | @Test 13 | void testServerCertificateTrustStoreExists() throws Exception { 14 | TestKeystores.TEST_SERVER_CERTIFICATE_TRUST.getKeystore(); 15 | } 16 | 17 | @Test 18 | void testClientCertificateKeyStoreExists() throws Exception { 19 | TestKeystores.TEST_CLIENT_CERTIFICATE.getKeystore(); 20 | } 21 | 22 | @Test 23 | void testClientCertificateTrustStoreExists() throws Exception { 24 | TestKeystores.TEST_CLIENT_CERTIFICATE_TRUST.getKeystore(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /readme/RELEASE_NOTES.md: -------------------------------------------------------------------------------- 1 | [![Maven Central](https://img.shields.io/static/v1?label=MavenCentral&message=${project.version}&color=blue)](https://search.maven.org/artifact/${project.groupId}/${project.artifactId}/${project.version}/jar) [![JavaDoc](https://img.shields.io/static/v1?label=JavaDoc&message=${project.version}&color=orange)](http://www.javadoc.io/doc/${project.groupId}/${project.artifactId}/${project.version}) 2 | 3 | ### Bug Fixes: 4 | * Fixed release process 5 | TEst 6 | 7 | 8 | ### Maven Central coordinates for this release 9 | 10 | ```xml 11 | 12 | ${project.groupId} 13 | ${project.artifactId} 14 | ${project.version} 15 | test 16 | 17 | ``` 18 | 19 | ### Gradle coordinates for this release 20 | 21 | ``` 22 | testImplementation '${project.groupId}:${project.artifactId}:${project.version}' 23 | ``` -------------------------------------------------------------------------------- /examples/src/main/java/de/skuzzle/springboot/test/example/ApiClientProperties.java: -------------------------------------------------------------------------------- 1 | package de.skuzzle.springboot.test.example; 2 | 3 | import org.springframework.boot.context.properties.ConfigurationProperties; 4 | import org.springframework.boot.context.properties.ConstructorBinding; 5 | 6 | @ConstructorBinding 7 | @ConfigurationProperties("api") 8 | class ApiClientProperties { 9 | 10 | private final String baseUrl; 11 | private final String username; 12 | private final String password; 13 | 14 | ApiClientProperties(String baseUrl, String username, String password) { 15 | this.baseUrl = baseUrl; 16 | this.username = username; 17 | this.password = password; 18 | } 19 | 20 | public String baseUrl() { 21 | return this.baseUrl; 22 | } 23 | 24 | public String username() { 25 | return this.username; 26 | } 27 | 28 | public String password() { 29 | return this.password; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /examples/src/main/java/de/skuzzle/springboot/test/example/ApiClientConfiguration.java: -------------------------------------------------------------------------------- 1 | package de.skuzzle.springboot.test.example; 2 | 3 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 4 | import org.springframework.boot.web.client.RestTemplateBuilder; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.web.client.RestTemplate; 8 | 9 | @Configuration 10 | @EnableConfigurationProperties(ApiClientProperties.class) 11 | class ApiClientConfiguration { 12 | 13 | @Bean 14 | public ApiClient apiClient(ApiClientProperties properties) { 15 | final RestTemplate restTemplate = new RestTemplateBuilder() 16 | .rootUri(properties.baseUrl()) 17 | .basicAuthentication(properties.username(), properties.password()) 18 | .build(); 19 | return new ApiClient(restTemplate); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /spring-boot-wiremock/src/main/java/de/skuzzle/springboot/test/wiremock/WiremockContextCustomizerFactory.java: -------------------------------------------------------------------------------- 1 | package de.skuzzle.springboot.test.wiremock; 2 | 3 | import java.util.List; 4 | 5 | import org.springframework.test.context.ContextConfigurationAttributes; 6 | import org.springframework.test.context.ContextCustomizer; 7 | import org.springframework.test.context.ContextCustomizerFactory; 8 | 9 | /** 10 | * Bootstraps the WireMock integration using {@link WiremockContextCustomizer} if the 11 | * {@link WithWiremock} annotation is detected on a test class. 12 | * 13 | * @author Simon Taddiken 14 | */ 15 | class WiremockContextCustomizerFactory implements ContextCustomizerFactory { 16 | 17 | @Override 18 | public ContextCustomizer createContextCustomizer(Class testClass, 19 | List configAttributes) { 20 | 21 | return WiremockAnnotationConfiguration 22 | .fromAnnotatedElement(testClass) 23 | .map(WiremockContextCustomizer::new) 24 | .orElse(null); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Simon Taddiken 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /spring-boot-wiremock/src/test/java/de/skuzzle/springboot/test/wiremock/metaannotations/MetaAnnotatedTest.java: -------------------------------------------------------------------------------- 1 | package de.skuzzle.springboot.test.wiremock.metaannotations; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import org.junit.jupiter.api.Test; 6 | import org.springframework.beans.factory.annotation.Value; 7 | import org.springframework.boot.test.context.SpringBootTest; 8 | import org.springframework.http.HttpStatus; 9 | import org.springframework.http.ResponseEntity; 10 | import org.springframework.web.client.RestTemplate; 11 | 12 | import de.skuzzle.springboot.test.wiremock.client.TestClients; 13 | 14 | @SpringBootTest 15 | @WithSampleServiceMock 16 | public class MetaAnnotatedTest { 17 | 18 | @Value("${sample-service.url}") 19 | private String sampleServiceUrl; 20 | 21 | private RestTemplate client() { 22 | return TestClients.restTemplate() 23 | .withBaseUrl(sampleServiceUrl) 24 | .withBasicAuth("user", "password") 25 | .build(); 26 | } 27 | 28 | @Test 29 | void testGetInfo() throws Exception { 30 | final ResponseEntity entity = client().getForEntity("/info", Object.class); 31 | assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /spring-boot-wiremock/src/test/java/de/skuzzle/springboot/test/wiremock/WireMockInitializerHttpTest.java: -------------------------------------------------------------------------------- 1 | package de.skuzzle.springboot.test.wiremock; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import org.junit.jupiter.api.Test; 6 | import org.springframework.beans.factory.annotation.Value; 7 | import org.springframework.boot.test.context.SpringBootTest; 8 | import org.springframework.http.HttpStatus; 9 | import org.springframework.http.ResponseEntity; 10 | import org.springframework.web.client.RestTemplate; 11 | 12 | import de.skuzzle.springboot.test.wiremock.client.TestClients; 13 | import de.skuzzle.springboot.test.wiremock.stubs.HttpStub; 14 | 15 | @SpringBootTest 16 | @HttpStub 17 | @WithWiremock(injectHttpHostInto = "serviceUrl") 18 | public class WireMockInitializerHttpTest { 19 | 20 | @Value("${serviceUrl}") 21 | private String serviceUrl; 22 | 23 | private RestTemplate client() { 24 | return TestClients.restTemplate() 25 | .withBaseUrl(serviceUrl) 26 | .build(); 27 | } 28 | 29 | @Test 30 | void testCallWiremockWithRestTemplate() throws Exception { 31 | final ResponseEntity response = client() 32 | .getForEntity("/", null, String.class); 33 | assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /examples/src/test/java/de/skuzzle/springboot/test/example/ApiClientTest.java: -------------------------------------------------------------------------------- 1 | package de.skuzzle.springboot.test.example; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import org.junit.jupiter.api.Test; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.boot.test.context.SpringBootTest; 8 | 9 | import de.skuzzle.springboot.test.wiremock.WithWiremock; 10 | import de.skuzzle.springboot.test.wiremock.stubs.Auth; 11 | import de.skuzzle.springboot.test.wiremock.stubs.HttpStub; 12 | import de.skuzzle.springboot.test.wiremock.stubs.Request; 13 | import de.skuzzle.springboot.test.wiremock.stubs.Response; 14 | 15 | @SpringBootTest(properties = { "api.username=user", "api.password=pw" }) 16 | @WithWiremock(injectHttpHostInto = "api.baseUrl", 17 | withGlobalAuthentication = @Auth(basicAuthUsername = "user", basicAuthPassword = "pw")) 18 | class ApiClientTest { 19 | 20 | @Autowired 21 | private ApiClient apiClient; 22 | 23 | @Test 24 | @HttpStub( 25 | onRequest = @Request(withMethod = "GET", toUrlPath = "/"), 26 | respond = @Response(withBody = "results")) 27 | void testGetResultsFromBackend() { 28 | final String resultsFromBackend = apiClient.getResultsFromBackend(); 29 | assertThat(resultsFromBackend).isEqualTo("results"); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /spring-boot-wiremock/src/main/java/de/skuzzle/springboot/test/wiremock/stubs/Auth.java: -------------------------------------------------------------------------------- 1 | package de.skuzzle.springboot.test.wiremock.stubs; 2 | 3 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 4 | 5 | import java.lang.annotation.Retention; 6 | 7 | import org.apiguardian.api.API; 8 | import org.apiguardian.api.API.Status; 9 | 10 | /** 11 | * For defining authentication information for a stub. If no attributes are specified, no 12 | * authentication will be required in order for the stub to match. 13 | * 14 | * @author Simon Taddiken 15 | * @see Request 16 | */ 17 | @API(status = Status.EXPERIMENTAL) 18 | @Retention(RUNTIME) 19 | public @interface Auth { 20 | 21 | /** 22 | * Required basic auth user name. Only taken into consideration if 23 | * {@link #basicAuthPassword()} is also configured. Mutual exclusive to 24 | * {@link #bearerToken()}. 25 | */ 26 | String basicAuthUsername() default ""; 27 | 28 | /** 29 | * Required basic auth user password. Only taken into consideration if 30 | * {@link #basicAuthUsername()} is also configured. Mutual exclusive to 31 | * {@link #bearerToken()}. 32 | */ 33 | String basicAuthPassword() default ""; 34 | 35 | /** 36 | * Required bearer token (case insensitive). Mutual exclusive to 37 | * {@link #basicAuthUsername()} and {@link #basicAuthPassword()}. 38 | */ 39 | String bearerToken() default ""; 40 | } 41 | -------------------------------------------------------------------------------- /spring-boot-wiremock/src/test/java/de/skuzzle/springboot/test/wiremock/WireMockInitializerHttpsTest.java: -------------------------------------------------------------------------------- 1 | package de.skuzzle.springboot.test.wiremock; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import org.junit.jupiter.api.Test; 6 | import org.springframework.beans.factory.annotation.Value; 7 | import org.springframework.boot.test.context.SpringBootTest; 8 | import org.springframework.http.HttpStatus; 9 | import org.springframework.http.ResponseEntity; 10 | import org.springframework.web.client.RestTemplate; 11 | 12 | import de.skuzzle.springboot.test.wiremock.client.TestClients; 13 | import de.skuzzle.springboot.test.wiremock.stubs.HttpStub; 14 | 15 | @SpringBootTest 16 | @WithWiremock(injectHttpsHostInto = "serviceUrl", randomHttpsPort = true, sslOnly = true) 17 | public class WireMockInitializerHttpsTest { 18 | 19 | @Value("${serviceUrl}") 20 | private String serviceUrl; 21 | 22 | private RestTemplate client() { 23 | return TestClients.restTemplate() 24 | .trusting(TestKeystores.TEST_SERVER_CERTIFICATE_TRUST.getKeystore()) 25 | .withBaseUrl(serviceUrl) 26 | .build(); 27 | } 28 | 29 | @Test 30 | @HttpStub 31 | void testCallWiremockWithRestTemplate() throws Exception { 32 | final ResponseEntity response = client() 33 | .getForEntity("/", null, String.class); 34 | assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /JenkinsfileJdkTests: -------------------------------------------------------------------------------- 1 | pipeline { 2 | agent none 3 | stages { 4 | stage('JDK-14') { 5 | agent { 6 | docker { 7 | image 'maven:3.6-jdk-14' 8 | args '-v /home/jenkins/.m2:/var/maven/.m2 -v /home/jenkins/.gnupg:/.gnupg -e MAVEN_CONFIG=/var/maven/.m2 -e MAVEN_OPTS=-Duser.home=/var/maven' 9 | } 10 | } 11 | steps { 12 | testAgainstJdk("14") 13 | } 14 | } 15 | stage('JDK-17') { 16 | agent { 17 | docker { 18 | image 'maven:3.6-jdk-17' 19 | args '-v /home/jenkins/.m2:/var/maven/.m2 -v /home/jenkins/.gnupg:/.gnupg -e MAVEN_CONFIG=/var/maven/.m2 -e MAVEN_OPTS=-Duser.home=/var/maven' 20 | } 21 | } 22 | steps { 23 | testAgainstJdk("17") 24 | } 25 | } 26 | } 27 | } 28 | 29 | void testAgainstJdk(version) { 30 | stage("Show Versions") { 31 | script { 32 | sh 'mvn -version' 33 | sh 'java -version' 34 | sh 'javac -version' 35 | } 36 | } 37 | 38 | stage("Clean Maven Project") { 39 | script { 40 | sh 'mvn clean -Dmaven.clean.failOnError=false -Dmaven.clean.retryOnError=true' 41 | } 42 | } 43 | 44 | stage("Test against JDK $version") { 45 | script { 46 | try { 47 | sh "mvn verify -Dmaven.compiler.release=$version" 48 | } catch (err) { 49 | currentBuild.result = 'FAILURE' 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /spring-boot-wiremock/src/main/java/de/skuzzle/springboot/test/wiremock/stubs/WrapAround.java: -------------------------------------------------------------------------------- 1 | package de.skuzzle.springboot.test.wiremock.stubs; 2 | 3 | import org.apiguardian.api.API; 4 | import org.apiguardian.api.API.Status; 5 | 6 | /** 7 | * Defines the response behavior in case a {@link HttpStub} has multiple responses. 8 | * 9 | * @author Simon Taddiken 10 | */ 11 | @API(status = Status.EXPERIMENTAL) 12 | public enum WrapAround { 13 | /** 14 | * If the mock reached the last defined response, it will respond with a 403 status 15 | * for every subsequent request. 16 | */ 17 | RETURN_ERROR { 18 | @Override 19 | public int determineNextState(int currentState, boolean hasNext) { 20 | return currentState + 1; 21 | } 22 | }, 23 | /** 24 | * If the mock reached the last defined response, it will start over with the first 25 | * defined response. 26 | */ 27 | START_OVER { 28 | 29 | @Override 30 | public int determineNextState(int currentState, boolean hasNext) { 31 | return hasNext ? currentState + 1 : 0; 32 | } 33 | 34 | }, 35 | /** 36 | * If the mock reached the last defined response, it will repeat it forever for every 37 | * subsequent request. 38 | */ 39 | REPEAT { 40 | 41 | @Override 42 | public int determineNextState(int currentState, boolean hasNext) { 43 | return hasNext 44 | ? currentState + 1 45 | : currentState; 46 | } 47 | 48 | }; 49 | 50 | @API(status = Status.INTERNAL) 51 | public abstract int determineNextState(int currentState, boolean hasNext); 52 | } 53 | -------------------------------------------------------------------------------- /spring-boot-wiremock/src/main/java/de/skuzzle/springboot/test/wiremock/StringValuePatterns.java: -------------------------------------------------------------------------------- 1 | package de.skuzzle.springboot.test.wiremock; 2 | 3 | import java.util.LinkedHashMap; 4 | import java.util.Map; 5 | import java.util.function.Function; 6 | 7 | import com.github.tomakehurst.wiremock.client.WireMock; 8 | import com.github.tomakehurst.wiremock.matching.StringValuePattern; 9 | 10 | final class StringValuePatterns { 11 | 12 | private StringValuePatterns() { 13 | // hidden 14 | } 15 | 16 | private static final Map> builders; 17 | static { 18 | final Map> temp = new LinkedHashMap<>(); 19 | temp.put("eq:", WireMock::equalTo); 20 | temp.put("eqIgnoreCase:", WireMock::equalToIgnoreCase); 21 | temp.put("eqToJson:", WireMock::equalToJson); 22 | temp.put("eqToXml:", WireMock::equalToXml); 23 | temp.put("matching:", WireMock::matching); 24 | temp.put("matchingXPath:", WireMock::matchingXPath); 25 | temp.put("matchingJsonPath:", WireMock::matchingJsonPath); 26 | temp.put("notMatching:", WireMock::notMatching); 27 | temp.put("containing:", WireMock::containing); 28 | builders = Map.copyOf(temp); 29 | } 30 | 31 | public static StringValuePattern parseFromPrefix(String pattern) { 32 | return builders.entrySet().stream() 33 | .filter(entry -> pattern.startsWith(entry.getKey())) 34 | .map(entry -> entry.getValue().apply(pattern.substring(entry.getKey().length()))) 35 | .findFirst() 36 | .orElseGet(() -> WireMock.equalTo(pattern)); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /spring-boot-wiremock/src/test/java/de/skuzzle/springboot/test/wiremock/metaannotations/WithSampleServiceMock.java: -------------------------------------------------------------------------------- 1 | package de.skuzzle.springboot.test.wiremock.metaannotations; 2 | 3 | import static java.lang.annotation.ElementType.TYPE; 4 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 5 | 6 | import java.lang.annotation.Retention; 7 | import java.lang.annotation.Target; 8 | 9 | import org.springframework.http.HttpStatus; 10 | 11 | import de.skuzzle.springboot.test.wiremock.WithWiremock; 12 | import de.skuzzle.springboot.test.wiremock.stubs.Auth; 13 | import de.skuzzle.springboot.test.wiremock.stubs.HttpStub; 14 | import de.skuzzle.springboot.test.wiremock.stubs.Request; 15 | import de.skuzzle.springboot.test.wiremock.stubs.Response; 16 | import de.skuzzle.springboot.test.wiremock.stubs.WrapAround; 17 | 18 | @Retention(RUNTIME) 19 | @Target(TYPE) 20 | @WithWiremock(injectHttpHostInto = "sample-service.url", 21 | withGlobalAuthentication = @Auth( 22 | basicAuthUsername = "user", 23 | basicAuthPassword = "password")) 24 | @HttpStub(onRequest = @Request( 25 | toUrl = "/info"), 26 | respond = @Response( 27 | withStatus = HttpStatus.OK, 28 | withStatusMessage = "Everything is Ok")) 29 | @HttpStub(onRequest = @Request( 30 | toUrl = "/submit/entity", 31 | withMethod = "PUT"), 32 | respond = { 33 | @Response(withStatus = HttpStatus.CREATED, withStatusMessage = "Entity created"), 34 | @Response(withStatus = HttpStatus.OK, withStatusMessage = "Entity already exists") 35 | }, onLastResponse = WrapAround.REPEAT) 36 | public @interface WithSampleServiceMock { 37 | 38 | } 39 | -------------------------------------------------------------------------------- /spring-boot-wiremock/src/test/java/de/skuzzle/springboot/test/wiremock/HelloWorldTest.java: -------------------------------------------------------------------------------- 1 | package de.skuzzle.springboot.test.wiremock; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import org.junit.jupiter.api.Test; 6 | import org.springframework.beans.factory.annotation.Value; 7 | import org.springframework.boot.test.context.SpringBootTest; 8 | import org.springframework.http.HttpStatus; 9 | import org.springframework.web.client.RestTemplate; 10 | 11 | import de.skuzzle.springboot.test.wiremock.stubs.HttpStub; 12 | import de.skuzzle.springboot.test.wiremock.stubs.Request; 13 | import de.skuzzle.springboot.test.wiremock.stubs.Response; 14 | 15 | @SpringBootTest 16 | @WithWiremock(injectHttpHostInto = "serviceUrl") 17 | public class HelloWorldTest { 18 | 19 | @Value("${serviceUrl}") 20 | private String serviceUrl; 21 | 22 | @Test 23 | @HttpStub( 24 | onRequest = @Request(withMethod = "POST"), 25 | respond = @Response( 26 | withStatus = HttpStatus.CREATED, 27 | withBody = "{\"value\": \"Hello World\"}", 28 | withContentType = "application/json")) 29 | void testCallWiremockWithRestTemplate() throws Exception { 30 | final var response = new RestTemplate().postForEntity(serviceUrl, null, HelloWorld.class); 31 | assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED); 32 | assertThat(response.getBody().getValue()).isEqualTo("Hello World"); 33 | } 34 | 35 | static class HelloWorld { 36 | private String value; 37 | 38 | public String getValue() { 39 | return this.value; 40 | } 41 | 42 | public void setValue(String value) { 43 | this.value = value; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | pipeline { 2 | agent { 3 | docker { 4 | image 'maven:3.8-jdk-11' 5 | args '-v /home/jenkins/.m2:/var/maven/.m2 -v /home/jenkins/.gnupg:/.gnupg -e MAVEN_CONFIG=/var/maven/.m2 -e MAVEN_OPTS=-Duser.home=/var/maven' 6 | } 7 | } 8 | environment { 9 | COVERALLS_REPO_TOKEN = credentials('coveralls_repo_token_spring_boot_wiremock') 10 | GPG_SECRET = credentials('gpg_password') 11 | } 12 | stages { 13 | stage ('Test Spring-Boot Compatibility') { 14 | steps { 15 | script { 16 | def versionsAsString = sh(script: 'mvn org.apache.maven.plugins:maven-help-plugin:3.2.0:evaluate -Dexpression=compatible-spring-boot-versions -q -DforceStdout', returnStdout:true).trim() 17 | def versionsArray = versionsAsString.split(',') 18 | versionsArray.each { 19 | stage("Verify against Spring-Boot ${it}") { 20 | sh "mvn -B clean verify -Dversion.spring-boot=${it.trim()}" 21 | } 22 | } 23 | } 24 | } 25 | } 26 | stage('Build Final') { 27 | steps { 28 | sh 'mvn -B clean verify' 29 | } 30 | } 31 | stage('Coverage') { 32 | steps { 33 | sh 'mvn -B jacoco:report jacoco:report-integration coveralls:report -DrepoToken=$COVERALLS_REPO_TOKEN' 34 | } 35 | } 36 | stage('javadoc') { 37 | steps { 38 | sh 'mvn -B javadoc:javadoc' 39 | } 40 | } 41 | stage('Deploy SNAPSHOT') { 42 | when { 43 | branch 'dev' 44 | } 45 | steps { 46 | sh 'mvn -B -Prelease -DskipTests -Dgpg.passphrase=${GPG_SECRET} deploy' 47 | } 48 | } 49 | } 50 | post { 51 | always { 52 | archiveArtifacts(artifacts: '*.md') 53 | junit (testResults: 'target/surefire-reports/*.xml', allowEmptyResults: true) 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /spring-boot-wiremock/src/main/java/de/skuzzle/springboot/test/wiremock/stubs/Response.java: -------------------------------------------------------------------------------- 1 | package de.skuzzle.springboot.test.wiremock.stubs; 2 | 3 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 4 | 5 | import java.lang.annotation.Retention; 6 | 7 | import org.apiguardian.api.API; 8 | import org.apiguardian.api.API.Status; 9 | import org.springframework.http.HttpStatus; 10 | 11 | /** 12 | * Defines the contents of the mock response that will be sent when the request of a 13 | * {@link HttpStub stub} was matched. When no attributes are defined, the response will be 14 | * empty with status 200 OK. 15 | * 16 | * @author Simon Taddiken 17 | */ 18 | @API(status = Status.EXPERIMENTAL) 19 | @Retention(RUNTIME) 20 | public @interface Response { 21 | 22 | /** The HTTP status of the response. Defaults to {@link HttpStatus#OK}. */ 23 | HttpStatus withStatus() default HttpStatus.OK; 24 | 25 | /** The HTTP status line. */ 26 | String withStatusMessage() default ""; 27 | 28 | /** 29 | * The body of the response as json string. Will also set the Content-Type to 30 | * application/json. The Content-Type can still be overridden by 31 | * {@link #withContentType()} or {@link #withHeaders()}. 32 | *

33 | * Mutual exclusive to {@link #withBody()}, {@link #withBodyFile()} and 34 | * {@link #withBodyBase64()}. Defaults to 'no body'. 35 | * 36 | * @since 0.0.17 37 | */ 38 | String withJsonBody() default ""; 39 | 40 | /** 41 | * The body of the response. Mutual exclusive to {@link #withBodyBase64()}, 42 | * {@link #withJsonBody()} and {@link #withBodyFile()}. Defaults to 'no body'. 43 | */ 44 | String withBody() default ""; 45 | 46 | /** 47 | * The body of the response. Mutual exclusive to {@link #withBody()}, 48 | * {@link #withJsonBody()} and {@link #withBodyFile()}. Defaults to 'no body'. 49 | */ 50 | String withBodyBase64() default ""; 51 | 52 | /** 53 | * The body of the response. Mutual exclusive to {@link #withBody()}, 54 | * {@link #withJsonBody()} and {@link #withBodyBase64()}. Defaults to 'no body'. 55 | *

56 | * By default, files must be contained in a folder on the classpath called 57 | * {@code __files}. 58 | */ 59 | String withBodyFile() default ""; 60 | 61 | /** 62 | * Content-Type for the response. If configured, this value takes precedence if 63 | * {@code "Content-Type"} is also configured using {@link #withHeaders()}. 64 | */ 65 | String withContentType() default ""; 66 | 67 | /** 68 | * Headers that will be added to the response. Specify pairs like 69 | * {@code "Content-Type=application/json"} 70 | */ 71 | String[] withHeaders() default {}; 72 | } 73 | -------------------------------------------------------------------------------- /CHANGELOG_LEGACY.md: -------------------------------------------------------------------------------- 1 | 2 | This changelog is no longer maintained. Follow the release notes at the GitHub releases for latest changes 3 | 4 | ## Changelog 5 | 6 | ### Version 0.0.14 7 | * [Dependency] Update to WireMock 2.27.2 8 | 9 | ### Version 0.0.13 10 | * Improve documentation 11 | * [Change] Move stubbing annotations into their own package: `de.skuzzle.wiremock.test.stubs` (**breaking**) 12 | * [Change] Deprecated `HttpStub.wrapAround` and introduced `HttpStub.onLastResponse` with new enum `WrapAround` 13 | * [Add] New properties that will always be injected: `wiremock.server.http(s)Host`, `wiremock.server.http(s)Port` 14 | * [Add] `WrapAround.REPEAT` which will repeat the last response on every subsequent request 15 | * [Add] Allow to globally define required authentication via `WithWiremock.withGlobalAuthentication` 16 | 17 | 18 | ### Version 0.0.12 19 | * Just some improvements to the build/release process 20 | 21 | ### Version 0.0.11 22 | * Just some improvements to the build/release process 23 | 24 | ### Version 0.0.10 25 | * [Fix] Readme 26 | * [Change] Use latest WireMock version (`2.27.1`) 27 | 28 | ### Version 0.0.9 29 | * [Add] Possibility to set a stub's priority 30 | * [Add] Allow to define annotation stubs on inherited super classes and interfaces of the test class 31 | * [Add] Allow to define annotation stubs using meta-annotated custom annotations 32 | * [Fix] Possibility to place multiple stubs on the test class (missing `target = { ..., ElementType.TYPE }` on `HttpStubs`) 33 | 34 | ### Version 0.0.8 35 | * Allow to configure consecutive responses for the same request 36 | 37 | ### Version 0.0.7 38 | * Compatibility to older Spring-Boot versions 39 | * Remove note about Junit 5 being required. This library actually isn't tied to a specific testing framework 40 | 41 | ### Version 0.0.6 42 | * Improve JavaDoc 43 | * Add automatic module name to jar manifest 44 | 45 | ### Version 0.0.5 46 | * Improve JavaDoc 47 | * Improve configuration consistency checks 48 | * Allow `@HttpStub` on test class itself (instead of only on test method) 49 | * Allow to set _status message_ on mock response 50 | * Allow to configure WireMock _scenarios_ for stateful request matching using annotations 51 | 52 | ### Version 0.0.4 53 | * Skipped by accident 🤡 54 | 55 | ### Version 0.0.3 56 | * Renamed `SimpleStub` to `HttpStub` and split into multiple annotations 57 | * `HttpStatus` enum is now used for defining the stubbed response status 58 | * Match _any_ HTTP method by default (instead of _GET_) 59 | * Allow to define different matchers for params, cookies, headers and body using prefixes like `eq:` or `containing:` 60 | 61 | ### Version 0.0.2 62 | * Support multiple `@SimpleStub` instances per test method 63 | * Allow to stub authentication and response headers via `@SimpleStub` 64 | * Fix bug with unresolvable test keystore locations 65 | 66 | ### Version 0.0.1 67 | * Initial prototype 68 | -------------------------------------------------------------------------------- /JenkinsfileRelease: -------------------------------------------------------------------------------- 1 | pipeline { 2 | agent { 3 | docker { 4 | image 'maven:3.8-jdk-11' 5 | args '-v /home/jenkins/.m2:/var/maven/.m2 -v /home/jenkins/.gnupg:/.gnupg -e MAVEN_CONFIG=/var/maven/.m2 -e MAVEN_OPTS=-Duser.home=/var/maven' 6 | } 7 | } 8 | environment { 9 | GPG_SECRET = credentials('gpg_password') 10 | GITHUB = credentials('Github-Username-Pw') 11 | GITHUB_RELEASE_TOKEN = credentials('github_registry_release') 12 | GIT_ASKPASS='./.git-askpass' 13 | } 14 | stages { 15 | stage ('Ensure dev branch') { 16 | when { 17 | expression { 18 | return env.BRANCH_NAME != 'dev'; 19 | } 20 | } 21 | steps { 22 | error("Releasing is only possible from dev branch") 23 | } 24 | } 25 | stage ('Set Git Information') { 26 | steps { 27 | sh 'echo \'echo \$GITHUB_PSW\' > ./.git-askpass' 28 | sh 'chmod +x ./.git-askpass' 29 | sh 'git config url."https://api@github.com/".insteadOf "https://github.com/"' 30 | sh 'git config url."https://ssh@github.com/".insteadOf "ssh://git@github.com/"' 31 | sh 'git config url."https://git@github.com/".insteadOf "git@github.com:"' 32 | sh 'git config user.email "build@taddiken.online"' 33 | sh 'git config user.name "Jenkins"' 34 | } 35 | } 36 | stage('Create release branch') { 37 | steps { 38 | sh 'mvn -B -Prelease gitflow:release-start' 39 | } 40 | } 41 | 42 | stage ('Test Spring-Boot Compatibility') { 43 | steps { 44 | script { 45 | def versionsAsString = sh(script: 'mvn org.apache.maven.plugins:maven-help-plugin:3.2.0:evaluate -Dexpression=compatible-spring-boot-versions -q -DforceStdout', returnStdout:true).trim() 46 | def versionsArray = versionsAsString.split(',') 47 | versionsArray.each { 48 | stage("Verify against Spring-Boot ${it}") { 49 | sh "mvn -B -Prelease -Dgpg.passphrase=${GPG_SECRET} clean verify -Dversion.spring-boot=${it.trim()}" 50 | } 51 | } 52 | } 53 | } 54 | } 55 | stage('Verify Release') { 56 | steps { 57 | sh 'mvn -B -Prelease -Dgpg.passphrase=${GPG_SECRET} verify' 58 | } 59 | } 60 | stage('Update readme') { 61 | steps { 62 | sh 'git add README.md RELEASE_NOTES.md' 63 | sh 'git commit -m "Update README and RELEASE_NOTES"' 64 | } 65 | } 66 | stage('Perform release') { 67 | steps { 68 | sh "mvn -B gitflow:release-finish -DargLine=\"-Prelease -B -Dgpg.passphrase=${GPG_SECRET} -DskipTests\"" 69 | } 70 | } 71 | stage('Create GitHub release') { 72 | steps { 73 | sh 'git checkout main' 74 | sh "mvn -B github-release:github-release -Dgithub.release-token=${GITHUB_RELEASE_TOKEN}" 75 | } 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /examples/pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | de.skuzzle.springboot.test 6 | spring-boot-wiremock-parent 7 | 0.0.18 8 | 9 | 10 | spring-boot-wiremock-examples 11 | 12 | 13 | 14 | de.skuzzle.springboot.test 15 | spring-boot-wiremock 16 | test 17 | 18 | 19 | 20 | 21 | org.springframework 22 | spring-context 23 | 24 | 25 | org.springframework 26 | spring-beans 27 | 28 | 29 | org.springframework 30 | spring-web 31 | provided 32 | 33 | 34 | 35 | org.springframework.boot 36 | spring-boot-starter 37 | compile 38 | 39 | 40 | org.springframework.boot 41 | spring-boot 42 | compile 43 | 44 | 45 | org.springframework.boot 46 | spring-boot-autoconfigure 47 | 48 | 49 | org.springframework.boot 50 | spring-boot-test 51 | 52 | 53 | org.junit.vintage 54 | junit-vintage-engine 55 | 56 | 57 | 58 | 59 | org.assertj 60 | assertj-core 61 | compile 62 | 63 | 64 | org.junit.jupiter 65 | junit-jupiter-api 66 | compile 67 | 68 | 69 | org.junit.jupiter 70 | junit-jupiter-params 71 | test 72 | 73 | 74 | 75 | 76 | 77 | 78 | maven-dependency-plugin 79 | 80 | 81 | org.springframework.boot:spring-boot-starter 82 | 83 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /spring-boot-wiremock/src/main/java/de/skuzzle/springboot/test/wiremock/stubs/HttpStub.java: -------------------------------------------------------------------------------- 1 | package de.skuzzle.springboot.test.wiremock.stubs; 2 | 3 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 4 | 5 | import java.lang.annotation.ElementType; 6 | import java.lang.annotation.Repeatable; 7 | import java.lang.annotation.Retention; 8 | import java.lang.annotation.Target; 9 | 10 | import org.apiguardian.api.API; 11 | import org.apiguardian.api.API.Status; 12 | 13 | import com.github.tomakehurst.wiremock.WireMockServer; 14 | 15 | /** 16 | * Allows to configure a simple, single stub for a test case by annotating the test method 17 | * or the test class. All attributes are optional. An empty {@code @HttpStub} on a test 18 | * method will set up a stub that returns a simple {@code 200 OK} response for every 19 | * incoming request. 20 | *

21 | * In order to refine the matching and the produced response, see {@link #onRequest()} and 22 | * {@link #respond()}. Here is a basic example: 23 | * 24 | *

25 |  * @Test
26 |  * @HttpStub(
27 |  *     onRequest = @Request(
28 |  *         toUrl("/createItem"),
29 |  *         withMethod("POST"),
30 |  *         withCookie("jsessionid=matching:[a-z0-9]+")
31 |  *         authenticatedBy = @Auth(
32 |  *             basicAuthUsername = "username",
33 |  *             basicAuthPassword = "password"))
34 |  *     respond = @Response(
35 |  *         withStatus(HttpStatus.CREATED),
36 |  *         withHeader("location="/newItem"),
37 |  *         withBody("{ \"status\": \"SUCCESS\" }")))
38 |  * void testCreateItem() {
39 |  *
40 |  * }
41 |  * 
42 | *

43 | * It is possible to configure multiple {@link #respond() responses}. If more than one 44 | * response is specified, the responses will be returned consecutively for each matched 45 | * request. 46 | *

47 | * The annotation can be put in various places: 48 | *

    49 | *
  • You can place the annotation on a single test method.
  • 50 | *
  • You can place the annotation on the test class itself to define global stubs.
  • 51 | *
  • You can place the annotation on a super class or any implemented interface of a 52 | * test class for easy reuse of the stub.
  • 53 | *
  • You can place the annotation as meta-annotation on a custom annotation type for 54 | * easy reuse of the stub.
  • 55 | *
  • The annotation is repeatable. Wherever you put a single instance, you can also put 56 | * multiple instances to define multiple stubs.
  • 57 | *
58 | * Note that all stubs are reset after each test. 59 | *

60 | * If you need more sophisticated stubbing, you can always just autowire the 61 | * {@link WireMockServer} into your test class and use 62 | * {@link WireMockServer#stubFor(com.github.tomakehurst.wiremock.client.MappingBuilder)}. 63 | * The same approach should be used to use verifications which are not supported via 64 | * annotations. 65 | * 66 | * @author Simon Taddiken 67 | * @see Request 68 | * @see Response 69 | */ 70 | @API(status = Status.EXPERIMENTAL) 71 | @Repeatable(HttpStubs.class) 72 | @Retention(RUNTIME) 73 | @Target({ ElementType.METHOD, ElementType.TYPE }) 74 | public @interface HttpStub { 75 | 76 | /** 77 | * The request that must be matched in order to produce a mock {@link #respond() 78 | * response}. By default, every request will be matched. 79 | */ 80 | Request onRequest() default @Request; 81 | 82 | /** 83 | * The mock responses that will consecutively be returned by the server if an incoming 84 | * request matched what has been configured in {@link #onRequest()}. By default, 85 | * returns an empty 200 OK message with no further details. 86 | */ 87 | Response[] respond() default { @Response }; 88 | 89 | /** 90 | * Defines the response behavior of the mock if multiple responses are defined. By 91 | * default, when the last response has been returned, the mock will answer with a 403 92 | * status code (see {@link WrapAround#RETURN_ERROR}). 93 | */ 94 | WrapAround onLastResponse() default WrapAround.RETURN_ERROR; 95 | } 96 | -------------------------------------------------------------------------------- /spring-boot-wiremock/src/test/java/de/skuzzle/springboot/test/wiremock/NestedTestTest.java: -------------------------------------------------------------------------------- 1 | package de.skuzzle.springboot.test.wiremock; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import org.junit.jupiter.api.Test; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.beans.factory.annotation.Value; 8 | import org.springframework.boot.context.properties.ConfigurationProperties; 9 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 10 | import org.springframework.boot.test.context.SpringBootTest; 11 | 12 | public class NestedTestTest { 13 | 14 | @SpringBootTest 15 | @WithWiremock(injectHttpHostInto = { "httpHost1", "httpHost2" }) 16 | static class TestInjectMultipleHttpHosts { 17 | 18 | @Value("${httpHost1}") 19 | private String host1; 20 | @Value("${httpHost2}") 21 | private String host2; 22 | 23 | @Test 24 | void testInjectHost1() throws Exception { 25 | assertThat(host1).startsWith("http:"); 26 | } 27 | 28 | @Test 29 | void testInjectHost2() throws Exception { 30 | assertThat(host2).startsWith("http:"); 31 | } 32 | } 33 | 34 | @SpringBootTest 35 | @WithWiremock(injectHttpsHostInto = { "httpsHost1", "httpsHost2" }, randomHttpsPort = true) 36 | static class TestInjectMultipleHttpsHosts { 37 | 38 | @Value("${httpsHost1}") 39 | private String host1; 40 | @Value("${httpsHost2}") 41 | private String host2; 42 | 43 | @Test 44 | void testInjectHost1() throws Exception { 45 | assertThat(host1).startsWith("https:"); 46 | } 47 | 48 | @Test 49 | void testInjectHost2() throws Exception { 50 | assertThat(host2).startsWith("https:"); 51 | } 52 | } 53 | 54 | @SpringBootTest 55 | @WithWiremock(fixedHttpPort = 13337) 56 | static class TestFixedHttpPort { 57 | @Value("${wiremock.server.httpHost}") 58 | private String host; 59 | 60 | @Test 61 | void testInjectHost1() throws Exception { 62 | assertThat(host).endsWith(":13337"); 63 | } 64 | } 65 | 66 | @SpringBootTest 67 | @WithWiremock(fixedHttpPort = 13338, randomHttpPort = true) 68 | static class TestFixedHttpPortTakesPrecedenceOverRandomHttpPort { 69 | @Value("${wiremock.server.httpHost}") 70 | private String host; 71 | 72 | @Test 73 | void testInjectHost() throws Exception { 74 | assertThat(host).endsWith(":13338"); 75 | } 76 | } 77 | 78 | @SpringBootTest 79 | @WithWiremock(fixedHttpsPort = 13339) 80 | static class TestFixedHttpsPort { 81 | @Value("${wiremock.server.httpsHost}") 82 | private String host; 83 | 84 | @Test 85 | void testInjectHost() throws Exception { 86 | assertThat(host).endsWith(":13339"); 87 | } 88 | } 89 | 90 | @SpringBootTest 91 | @WithWiremock(randomHttpsPort = true) 92 | static class TestRandomHttpsHost { 93 | @Value("${wiremock.server.httpsHost}") 94 | private String host; 95 | 96 | @Test 97 | void testContextStarts() throws Exception { 98 | } 99 | } 100 | 101 | @SpringBootTest 102 | @EnableConfigurationProperties(TestConfigurationProperties.class) 103 | @WithWiremock(injectHttpHostInto = "url.in.propertiesfile") 104 | static class InjectIntoConfigurationProperties { 105 | @Autowired 106 | private TestConfigurationProperties properties; 107 | @Value("${url.in.propertiesfile}") 108 | private String host; 109 | 110 | @Test 111 | void testWiremockLocationTakesPrecedenceOverStaticConfiguration() throws Exception { 112 | assertThat(properties.getPropertiesfile()).isEqualTo(host); 113 | } 114 | } 115 | 116 | @ConfigurationProperties("url.in") 117 | static class TestConfigurationProperties { 118 | private String propertiesfile; 119 | 120 | public String getPropertiesfile() { 121 | return this.propertiesfile; 122 | } 123 | 124 | public void setPropertiesfile(String propertiesfile) { 125 | this.propertiesfile = propertiesfile; 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /spring-boot-wiremock/src/test/java/de/skuzzle/springboot/test/wiremock/client/TestClients.java: -------------------------------------------------------------------------------- 1 | package de.skuzzle.springboot.test.wiremock.client; 2 | 3 | import java.security.KeyStore; 4 | import java.security.KeyStoreException; 5 | import java.security.NoSuchAlgorithmException; 6 | import java.security.UnrecoverableKeyException; 7 | import java.util.function.Function; 8 | 9 | import org.apache.http.impl.client.CloseableHttpClient; 10 | import org.apache.http.impl.client.HttpClientBuilder; 11 | import org.apache.http.ssl.SSLContextBuilder; 12 | import org.springframework.boot.web.client.RestTemplateBuilder; 13 | import org.springframework.http.client.ClientHttpRequestFactory; 14 | import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; 15 | import org.springframework.web.client.RestTemplate; 16 | 17 | public final class TestClients { 18 | 19 | public static ClientBuilder restTemplate() { 20 | return new RestTemplateClientBuilder(new RestTemplateBuilder()); 21 | } 22 | 23 | public static interface ClientBuilder { 24 | ClientBuilder customize(Function builder); 25 | 26 | ClientBuilder withBasicAuth(String username, String password); 27 | 28 | ClientBuilder withBaseUrl(String baseUrl); 29 | 30 | ClientBuilder withClientAuth(KeyStore keystore, char[] keyPassword); 31 | 32 | ClientBuilder trusting(KeyStore truststore); 33 | 34 | C build(); 35 | } 36 | 37 | public static final class RestTemplateClientBuilder implements ClientBuilder { 38 | 39 | private RestTemplateBuilder builder; 40 | private final SSLContextBuilder sslContextBuilder; 41 | 42 | public RestTemplateClientBuilder(RestTemplateBuilder builder) { 43 | this.builder = builder; 44 | this.sslContextBuilder = SSLContextBuilder.create(); 45 | } 46 | 47 | @Override 48 | public ClientBuilder customize( 49 | Function builder) { 50 | this.builder = builder.apply(this.builder); 51 | return this; 52 | } 53 | 54 | @Override 55 | public ClientBuilder withBasicAuth(String username, String password) { 56 | builder = builder.basicAuthentication(username, password); 57 | return this; 58 | } 59 | 60 | @Override 61 | public ClientBuilder withBaseUrl(String baseUrl) { 62 | builder = builder.rootUri(baseUrl); 63 | return this; 64 | } 65 | 66 | @Override 67 | public ClientBuilder withClientAuth(KeyStore keystore, char[] keyPassword) { 68 | try { 69 | sslContextBuilder.loadKeyMaterial(keystore, keyPassword); 70 | } catch (UnrecoverableKeyException | NoSuchAlgorithmException | KeyStoreException e) { 71 | throw new IllegalArgumentException("Error configuring keystore", e); 72 | } 73 | return this; 74 | } 75 | 76 | @Override 77 | public ClientBuilder trusting(KeyStore truststore) { 78 | try { 79 | sslContextBuilder.loadTrustMaterial(truststore, null); 80 | } catch (NoSuchAlgorithmException | KeyStoreException e) { 81 | throw new IllegalArgumentException("Error configuring truststore", e); 82 | } 83 | 84 | return this; 85 | } 86 | 87 | @Override 88 | public RestTemplate build() { 89 | try { 90 | final CloseableHttpClient httpClient = HttpClientBuilder.create() 91 | .setSSLContext(sslContextBuilder.build()) 92 | .build(); 93 | 94 | final ClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient); 95 | return builder 96 | .requestFactory(() -> requestFactory) 97 | .build(); 98 | } catch (final Exception e) { 99 | throw new IllegalArgumentException("Error configuring test client", e); 100 | } 101 | } 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /spring-boot-wiremock/src/main/java/de/skuzzle/springboot/test/wiremock/TestKeystores.java: -------------------------------------------------------------------------------- 1 | package de.skuzzle.springboot.test.wiremock; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | import java.net.URL; 6 | import java.security.KeyStore; 7 | import java.security.KeyStoreException; 8 | import java.security.NoSuchAlgorithmException; 9 | import java.security.cert.CertificateException; 10 | 11 | import org.apiguardian.api.API; 12 | import org.apiguardian.api.API.Status; 13 | 14 | import com.google.common.io.Resources; 15 | 16 | /** 17 | * Holds the locations for the default key- and truststores that are used for mock SSL 18 | * connections. These certificates can be used if no custom key- or truststore have been 19 | * configured in your test. 20 | *

21 | * Never use any of these in production. 22 | * 23 | * @author Simon Taddiken 24 | */ 25 | @API(status = Status.EXPERIMENTAL) 26 | public final class TestKeystores { 27 | 28 | private TestKeystores() { 29 | } 30 | 31 | /** 32 | * A keystore containing a client certificate that is considered valid by the mock 33 | * server. 34 | */ 35 | public static final KeystoreLocation TEST_CLIENT_CERTIFICATE = new KeystoreLocation( 36 | "certs/client_keystore.pkcs12", 37 | "password", 38 | "PKCS12"); 39 | /** 40 | * A truststore for trusting the client certificate contained in 41 | * {@link #TEST_CLIENT_CERTIFICATE}. 42 | */ 43 | public static final KeystoreLocation TEST_CLIENT_CERTIFICATE_TRUST = new KeystoreLocation( 44 | "certs/server_truststore.jks", 45 | "password", 46 | "JKS"); 47 | /** 48 | * A keystore containing a self signed server certificate which is used by the mock. 49 | */ 50 | public static final KeystoreLocation TEST_SERVER_CERTIFICATE = new KeystoreLocation( 51 | "certs/server_keystore.jks", 52 | "password", 53 | "JKS"); 54 | /** 55 | * A truststore for trusting the self signed server certificate contained in 56 | * {@link #TEST_SERVER_CERTIFICATE} 57 | */ 58 | public static final KeystoreLocation TEST_SERVER_CERTIFICATE_TRUST = new KeystoreLocation( 59 | "certs/client_truststore.jks", 60 | "password", 61 | "JKS"); 62 | 63 | /** 64 | * Information about a test keystore. 65 | * 66 | * @author Simon Taddiken 67 | */ 68 | public static final class KeystoreLocation { 69 | private final String classpathLocation; 70 | private final String password; 71 | private final String type; 72 | 73 | private KeystoreLocation(String classpathLocation, String password, String type) { 74 | this.classpathLocation = classpathLocation; 75 | this.password = password; 76 | this.type = type; 77 | } 78 | 79 | /** 80 | * Location resolved from classpath. 81 | * 82 | * @return The location as URL. 83 | */ 84 | public URL toURL() { 85 | return Resources.getResource(getClasspathLocation()); 86 | } 87 | 88 | /** 89 | * Location that can be resolved using {@link ClassLoader#getResource(String)}. 90 | * 91 | * @return The classpath location. 92 | */ 93 | public String getClasspathLocation() { 94 | return classpathLocation; 95 | } 96 | 97 | public String getLocation() { 98 | return toURL().toString(); 99 | } 100 | 101 | public String getPassword() { 102 | return this.password; 103 | } 104 | 105 | public String getType() { 106 | return this.type; 107 | } 108 | 109 | /** 110 | * Materializes the keystore from its location. 111 | * 112 | * @return The keystore. 113 | */ 114 | public KeyStore getKeystore() { 115 | try (InputStream in = toURL().openStream()) { 116 | final KeyStore keyStore = KeyStore.getInstance(getType()); 117 | keyStore.load(in, getPassword().toCharArray()); 118 | return keyStore; 119 | } catch (final KeyStoreException | IOException | NoSuchAlgorithmException | CertificateException e) { 120 | throw new IllegalStateException( 121 | "Could not read keystore from classpath location: " + classpathLocation); 122 | } 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | de.skuzzle 7 | skuzzle-parent 8 | 3.0.1 9 | 10 | 11 | de.skuzzle.springboot.test 12 | spring-boot-wiremock-parent 13 | 0.0.18 14 | https://github.com/skuzzle/spring-boot-wiremock 15 | pom 16 | 17 | 18 | false 19 | 20 | 21 | 2.2.13.RELEASE 22 | 2.27.2 23 | 30.1.1-jre 24 | 1.1.2 25 | 3.7.1 26 | 27 | 2.3.12.RELEASE, 2.4.11, 2.5.5 28 | 29 | spring-boot-wiremock 30 | spring-boot-wiremock 31 | 32 | 33 | 34 | 35 | spring-boot-wiremock 36 | examples 37 | 38 | 39 | 40 | 41 | 42 | de.skuzzle.springboot.test 43 | spring-boot-wiremock 44 | ${project.version} 45 | 46 | 47 | 48 | org.springframework.boot 49 | spring-boot-dependencies 50 | ${version.spring-boot} 51 | pom 52 | import 53 | 54 | 55 | com.github.tomakehurst 56 | wiremock-jre8 57 | ${version.wiremock} 58 | 59 | 60 | com.google.guava 61 | guava 62 | ${version.guava} 63 | 64 | 65 | org.apiguardian 66 | apiguardian-api 67 | ${version.api-guardian} 68 | 69 | 70 | nl.jqno.equalsverifier 71 | equalsverifier 72 | ${version.equalsverifier} 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | com.amashchenko.maven.plugin 81 | gitflow-maven-plugin 82 | false 83 | 84 | deploy 85 | true 86 | true 87 | true 88 | 89 | main 90 | dev 91 | 92 | 93 | 94 | 95 | com.ragedunicorn.tools.maven 96 | github-release-maven-plugin 97 | 1.0.2 98 | false 99 | 100 | skuzzle 101 | ${github.name} 102 | ${github.release-token} 103 | v${project.version} 104 | ${project.version} 105 | main 106 | RELEASE_NOTES.md 107 | 108 | 109 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /spring-boot-wiremock/src/main/java/de/skuzzle/springboot/test/wiremock/WiremockContextCustomizer.java: -------------------------------------------------------------------------------- 1 | package de.skuzzle.springboot.test.wiremock; 2 | 3 | import java.lang.reflect.AnnotatedElement; 4 | import java.util.Map; 5 | import java.util.Objects; 6 | import java.util.stream.Stream; 7 | 8 | import org.springframework.boot.test.util.TestPropertyValues; 9 | import org.springframework.context.ConfigurableApplicationContext; 10 | import org.springframework.context.event.ContextClosedEvent; 11 | import org.springframework.core.annotation.MergedAnnotation; 12 | import org.springframework.core.annotation.MergedAnnotations; 13 | import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; 14 | import org.springframework.test.context.ContextCustomizer; 15 | import org.springframework.test.context.MergedContextConfiguration; 16 | import org.springframework.test.context.TestContext; 17 | import org.springframework.test.context.event.AfterTestExecutionEvent; 18 | import org.springframework.test.context.event.BeforeTestExecutionEvent; 19 | 20 | import com.github.tomakehurst.wiremock.WireMockServer; 21 | import com.google.common.base.Preconditions; 22 | 23 | import de.skuzzle.springboot.test.wiremock.stubs.HttpStub; 24 | 25 | /** 26 | * Starts and manages the lifecycle of the WireMock server and injects its hosts into the 27 | * properties defined in {@link WithWiremock#injectHttpHostInto()} and 28 | * {@link WithWiremock#injectHttpsHostInto()}. 29 | * 30 | * @author Simon Taddiken 31 | */ 32 | final class WiremockContextCustomizer implements ContextCustomizer { 33 | 34 | private final WiremockAnnotationConfiguration wiremockProps; 35 | 36 | public WiremockContextCustomizer(WiremockAnnotationConfiguration wiremockProps) { 37 | Preconditions.checkArgument(wiremockProps != null, "props must not be null"); 38 | this.wiremockProps = wiremockProps; 39 | } 40 | 41 | @Override 42 | public void customizeContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) { 43 | final WireMockServer server = startServer(); 44 | injectHost(context, server); 45 | addLifecycleEvents(context, server); 46 | } 47 | 48 | private WireMockServer startServer() { 49 | final WireMockServer server = wiremockProps.createWireMockServer(); 50 | server.start(); 51 | return server; 52 | } 53 | 54 | private void injectHost(ConfigurableApplicationContext context, WireMockServer server) { 55 | final Map propertiesToInject = wiremockProps.determineInjectionPropertiesFrom(server); 56 | TestPropertyValues 57 | .of(toStringProps(propertiesToInject)) 58 | .applyTo(context); 59 | } 60 | 61 | private void addLifecycleEvents(ConfigurableApplicationContext applicationContext, 62 | WireMockServer wiremockServer) { 63 | applicationContext.addApplicationListener(applicationEvent -> { 64 | if (applicationEvent instanceof BeforeTestExecutionEvent) { 65 | final BeforeTestExecutionEvent e = (BeforeTestExecutionEvent) applicationEvent; 66 | 67 | final WithWiremock withWiremock = this.wiremockProps.annotation(); 68 | 69 | final TestContext testContext = e.getTestContext(); 70 | Stream.concat( 71 | determineStubs(testContext.getTestClass()), 72 | determineStubs(testContext.getTestMethod())) 73 | .forEach(stub -> StubTranslator.configureStubOn(wiremockServer, withWiremock, stub)); 74 | 75 | } 76 | if (applicationEvent instanceof AfterTestExecutionEvent) { 77 | wiremockServer.resetAll(); 78 | } 79 | if (applicationEvent instanceof ContextClosedEvent) { 80 | wiremockServer.stop(); 81 | } 82 | }); 83 | } 84 | 85 | private Stream determineStubs(AnnotatedElement e) { 86 | return MergedAnnotations.from(e, SearchStrategy.TYPE_HIERARCHY_AND_ENCLOSING_CLASSES) 87 | .stream(HttpStub.class) 88 | .map(MergedAnnotation::synthesize); 89 | 90 | } 91 | 92 | @Deprecated 93 | private Stream toStringProps(Map props) { 94 | // Only for compatibility to older Spring-Boot versions that do not support 95 | // TestPropertyValues.of(Map) 96 | // This method can be removed when the base-line spring-boot version is 2.4.x 97 | return props.entrySet().stream() 98 | .map(entry -> entry.getKey() + "=" + entry.getValue()); 99 | } 100 | 101 | @Override 102 | public int hashCode() { 103 | return Objects.hash(wiremockProps); 104 | } 105 | 106 | @Override 107 | public boolean equals(Object obj) { 108 | return obj == this || obj instanceof WiremockContextCustomizer 109 | && Objects.equals(wiremockProps, ((WiremockContextCustomizer) obj).wiremockProps); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /spring-boot-wiremock/src/main/java/de/skuzzle/springboot/test/wiremock/WithWiremock.java: -------------------------------------------------------------------------------- 1 | package de.skuzzle.springboot.test.wiremock; 2 | 3 | import static java.lang.annotation.ElementType.TYPE; 4 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 5 | 6 | import java.lang.annotation.Retention; 7 | import java.lang.annotation.Target; 8 | 9 | import org.apiguardian.api.API; 10 | import org.apiguardian.api.API.Status; 11 | import org.springframework.boot.test.context.SpringBootTest; 12 | 13 | import com.github.tomakehurst.wiremock.WireMockServer; 14 | 15 | import de.skuzzle.springboot.test.wiremock.stubs.Auth; 16 | import de.skuzzle.springboot.test.wiremock.stubs.HttpStub; 17 | import de.skuzzle.springboot.test.wiremock.stubs.Request; 18 | 19 | /** 20 | * Configures a WireMock server that is integrated with the Spring ApplicationContext. Use 21 | * in conjunction with any Spring Boot test annotation like {@link SpringBootTest}. You 22 | * should configure {@link #injectHttpHostInto()} resp. {@link #injectHttpsHostInto()} in 23 | * order to have the mock's random base url injected into a Spring-Boot application 24 | * property. 25 | *

26 | * By default, the mock server only serves unencrypted HTTP. If you want to test encrypted 27 | * traffic using SSL, you need to either specify 28 | * {@link #randomHttpsPort()}=true or {@link #fixedHttpsPort()} with a value 29 | * >= 0. 30 | *

31 | * The configured {@link WireMockServer} instance is made available in the application 32 | * context and thus can easily be injected into a test class like this: 33 | * 34 | *

 35 |  * @Autowired
 36 |  * private WireMockServer wiremock;
 37 |  * 
38 | * 39 | * Subsequently you can use it to configure your stubs if you refrain from using 40 | * {@link HttpStub annotation based} stubbing. Note that you should not call any lifecycle 41 | * methods like {@link WireMockServer#stop()} on the injected instance. The lifecycle will 42 | * be managed by this framework internally. 43 | * 44 | * @author Simon Taddiken 45 | * @see TestKeystores 46 | * @see HttpStub 47 | */ 48 | @API(status = Status.EXPERIMENTAL) 49 | @Retention(RUNTIME) 50 | @Target(TYPE) 51 | public @interface WithWiremock { 52 | 53 | static final int DEFAULT_HTTP_PORT = 0; 54 | static final int DEFAULT_HTTPS_PORT = -1; 55 | 56 | /** 57 | * The names of the application properties that will be added and contain the 58 | * wiremock's http url. 59 | */ 60 | String[] injectHttpsHostInto() default ""; 61 | 62 | /** 63 | * The names of the application properties that will be added and contain the 64 | * wiremock's https url. 65 | */ 66 | String[] injectHttpHostInto() default ""; 67 | 68 | /** 69 | * Whether client authentication (via SSL client certificate) is required. When 70 | * {@link #truststoreLocation()} is not configured then the mock server trusts the 71 | * single certificate that can be retrieved using 72 | * {@link TestKeystores#TEST_CLIENT_CERTIFICATE}. 73 | */ 74 | boolean needClientAuth() default false; 75 | 76 | /** 77 | * Location of the keystore to use for server side SSL. Defaults to 78 | * {@link TestKeystores#TEST_SERVER_CERTIFICATE}. The location will be resolved using 79 | * {@link ClassLoader#getResource(String)}. 80 | */ 81 | String keystoreLocation() default "certs/server_keystore.jks"; 82 | 83 | /** 84 | * Type of the {@link #keystoreLocation() keystore}. 85 | */ 86 | String keystoreType() default "JKS"; 87 | 88 | /** 89 | * Password of the {@link #keystoreLocation() keystore}. 90 | */ 91 | String keystorePassword() default "password"; 92 | 93 | /** 94 | * Location for the trustsore to use for client side SSL. Defaults to 95 | * {@link TestKeystores#TEST_CLIENT_CERTIFICATE_TRUST}. The location will be resolved 96 | * using {@link ClassLoader#getResource(String)}. 97 | */ 98 | String truststoreLocation() default "certs/server_truststore.jks"; 99 | 100 | /** 101 | * Password of the {@link #truststoreLocation() truststore}. 102 | */ 103 | String truststorePassword() default "password"; 104 | 105 | /** 106 | * Type of the {@link #truststoreLocation() truststore}. 107 | */ 108 | String truststoreType() default "JKS"; 109 | 110 | /** 111 | * Disable HTTP and only serves HTTPS. 112 | */ 113 | boolean sslOnly() default false; 114 | 115 | /** 116 | * Whether to use random HTTP port. Defaults to true but will be silently 117 | * ignored if {@link #fixedHttpPort()} is specified with a value > 0 118 | * 119 | * @since 0.0.15 120 | */ 121 | boolean randomHttpPort() default true; 122 | 123 | /** 124 | * Enables HTTPS on a random port. Defaults to false. Mutual exclusive to 125 | * {@link #fixedHttpsPort()}. 126 | * 127 | * @since 0.0.15 128 | */ 129 | boolean randomHttpsPort() default false; 130 | 131 | /** 132 | * Enables HTTP on a fixed port. If specified with a value > 0 the fixed port will 133 | * take precedence even if {@link #randomHttpPort()} is set to true. 134 | * 135 | * @since 0.0.15 136 | */ 137 | int fixedHttpPort() default DEFAULT_HTTP_PORT; 138 | 139 | /** 140 | * Enables HTTPS on a fixed port. Mutual exclusive to {@link #randomHttpsPort()}. 141 | * 142 | * @since 0.0.15 143 | */ 144 | int fixedHttpsPort() default DEFAULT_HTTPS_PORT; 145 | 146 | /** 147 | * Required authentication information that will be added to every stub which itself 148 | * doesn't specify {@link Request#authenticatedBy()}. Note that, once authentication 149 | * is configured on this level, you can not undo it for specific stubs. 150 | */ 151 | Auth withGlobalAuthentication() default @Auth; 152 | } 153 | -------------------------------------------------------------------------------- /spring-boot-wiremock/src/main/java/de/skuzzle/springboot/test/wiremock/stubs/Request.java: -------------------------------------------------------------------------------- 1 | package de.skuzzle.springboot.test.wiremock.stubs; 2 | 3 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 4 | 5 | import java.lang.annotation.Retention; 6 | 7 | import org.apiguardian.api.API; 8 | import org.apiguardian.api.API.Status; 9 | 10 | import com.github.tomakehurst.wiremock.client.WireMock; 11 | 12 | /** 13 | * Defines the stub request that will be matched in order to produce a mock response. If 14 | * no attributes are specified, every request will be matched. 15 | *

16 | * Some attributes of this annotation support advanced matching instead of plain text 17 | * comparison. For example, {@code withBody = "containing:someString"} matches a body that 18 | * contains {@code "someString"}. There are multiple such operators that are supported: 19 | * 20 | * 21 | * 22 | * 23 | * 24 | * 25 | * 26 | * 27 | * 28 | * 29 | * 30 | * 31 | * 32 | * 33 | * 34 | * 35 | * 36 | * 37 | * 38 | * 39 | * 40 | * 41 | * 42 | * 43 | * 44 | * 45 | * 46 | * 47 | * 48 | * 49 | * 50 | * 51 | * 52 | * 53 | * 54 | * 55 | * 56 | * 57 | * 58 | * 59 | * 60 | * 61 | * 62 | *
Supported string matching operations
prefixoperation
eq:{@link WireMock#equalTo(String)}
eqIgnoreCase:{@link WireMock#equalToIgnoreCase(String)}
eqToJson:{@link WireMock#equalToJson(String)}
eqToXml:{@link WireMock#equalToXml(String)}
matching:{@link WireMock#matching(String)}
notMatching:{@link WireMock#notMatching(String)}
matchingXPath:{@link WireMock#matchingXPath(String)}
matchingJsonPath:{@link WireMock#matchingJsonPath(String)}
containing:{@link WireMock#containing(String)}
63 | * With no prefix the string comparison defaults to eq:. 64 | * 65 | * @author Simon Taddiken 66 | * @see Response 67 | */ 68 | @API(status = Status.EXPERIMENTAL) 69 | @Retention(RUNTIME) 70 | public @interface Request { 71 | 72 | // sentinel value to signal that no priority was configured 73 | static int NO_PRIORITY = Integer.MAX_VALUE - 1; 74 | 75 | /** The stub's priority. */ 76 | int priority() default NO_PRIORITY; 77 | 78 | /** 79 | * Allows to configure WireMock scenarios that can be used for stateful request 80 | * matching. 81 | *

82 | * Note that this attribute must be left empty when used within a {@link HttpStub} 83 | * with multiple responses configured. 84 | */ 85 | Scenario scenario() default @Scenario; 86 | 87 | /** 88 | * Authentication information required for the stub to match. By default, no 89 | * authentication is required. 90 | */ 91 | Auth authenticatedBy() default @Auth; 92 | 93 | /** Request method for this stub. If not specified, every method will be matched. */ 94 | String withMethod() default "ANY"; 95 | 96 | /** 97 | * The URL for this stub. Mutual exclusive to {@link #toUrlPattern()}, 98 | * {@link #toUrlPath()} and {@link #toUrlPathPattern()}. If not specified, every url 99 | * will be matched. 100 | *

101 | * Warning: Using {@link #toUrl()} in combination with {@link #withQueryParameters()} 102 | * will effectively result in a conflicting stub definition that will never match. Use 103 | * {@link #toUrlPath()} instead. 104 | */ 105 | String toUrl() default ""; 106 | 107 | String toUrlPattern() default ""; 108 | 109 | /** 110 | * The URL path for this stub. Mutual exclusive to {@link #toUrlPattern()}, 111 | * {@link #toUrl()} and {@link #toUrlPathPattern()}. If not specified, every url will 112 | * be matched. 113 | */ 114 | String toUrlPath() default ""; 115 | 116 | String toUrlPathPattern() default ""; 117 | 118 | /** 119 | * The expected body to match. You can optionally prefix the string with a matching 120 | * operator like {@code containing:} or {@code matching:} By default, matches every 121 | * body. 122 | */ 123 | String withBody() default ""; 124 | 125 | /** 126 | * Headers to match. You can specify key/value pairs and optionally operators for 127 | * value matching like so: 128 | * 129 | *

130 |      * containingHeaders = {
131 |      *     "If-None-Match=matching:[a-z0-9-]",
132 |      *     "Content-Type=application/json"
133 |      * }
134 |      * 
135 | * 136 | * See the documentation for {@link Request} for a list of supported operators. 137 | */ 138 | String[] containingHeaders() default {}; 139 | 140 | /** 141 | * Cookies to match. You can specify key/value pairs and optionally operators for 142 | * value matching like so: 143 | * 144 | *
145 |      * containingCookies = {
146 |      *     "jsessionId=matching:[a-z0-9]"
147 |      * }
148 |      * 
149 | * 150 | * See the documentation for {@link Request} for a list of supported operators. 151 | */ 152 | String[] containingCookies() default {}; 153 | 154 | /** 155 | * Query parameters to match. You can specify key/value pairs and optionally operators 156 | * for value matching like so: 157 | * 158 | *
159 |      * withQueryParameters = {
160 |      *     "search=eqIgnoreCase:searchterm",
161 |      *     "limit=100"
162 |      * }
163 |      * 
164 | * 165 | * See the documentation for {@link Request} for a list of supported operators. 166 | *

167 | * Note: Doesn't work in combination with {@link #toUrl()} but you can use 168 | * {@link #toUrlPath()} instead. See related GitHub issue: 169 | * https://github.com/tomakehurst/wiremock/issues/1262 170 | */ 171 | String[] withQueryParameters() default {}; 172 | 173 | } 174 | -------------------------------------------------------------------------------- /spring-boot-wiremock/pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | de.skuzzle.springboot.test 6 | spring-boot-wiremock-parent 7 | 0.0.18 8 | 9 | 10 | spring-boot-wiremock 11 | SpringBoot WireMock 12 | Easily set up WireMock in your SpringBoot tests 13 | 14 | 15 | 16 | 17 | com.google.guava 18 | guava 19 | 20 | 21 | org.apiguardian 22 | apiguardian-api 23 | 24 | 25 | com.github.tomakehurst 26 | wiremock-jre8 27 | compile 28 | 29 | 30 | org.assertj 31 | assertj-core 32 | compile 33 | 34 | 35 | org.junit.jupiter 36 | junit-jupiter-api 37 | compile 38 | 39 | 40 | org.slf4j 41 | slf4j-api 42 | compile 43 | 44 | 45 | org.apache.httpcomponents 46 | httpclient 47 | test 48 | 49 | 50 | org.apache.httpcomponents 51 | httpcore 52 | test 53 | 54 | 55 | 56 | 57 | org.springframework 58 | spring-context 59 | 60 | 61 | org.springframework 62 | spring-core 63 | 64 | 65 | org.springframework 66 | spring-test 67 | 68 | 69 | org.springframework 70 | spring-beans 71 | 72 | 73 | org.springframework 74 | spring-web 75 | provided 76 | 77 | 78 | 79 | org.springframework.boot 80 | spring-boot-starter 81 | compile 82 | 83 | 84 | org.springframework.boot 85 | spring-boot 86 | compile 87 | 88 | 89 | org.springframework.boot 90 | spring-boot-test 91 | compile 92 | 93 | 94 | org.junit.vintage 95 | junit-vintage-engine 96 | 97 | 98 | 99 | 100 | org.springframework.boot 101 | spring-boot-autoconfigure 102 | 103 | 104 | org.junit.jupiter 105 | junit-jupiter-params 106 | test 107 | 108 | 109 | nl.jqno.equalsverifier 110 | equalsverifier 111 | test 112 | 113 | 114 | 115 | 116 | 117 | 118 | org.apache.maven.plugins 119 | maven-resources-plugin 120 | 3.2.0 121 | 122 | 123 | 124 | copy-resources 125 | 126 | validate 127 | 128 | ${basedir}/.. 129 | 130 | 131 | ${basedir}/../readme/ 132 | true 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | org.apache.maven.plugins 141 | maven-jar-plugin 142 | 143 | 144 | 145 | de.skuzzle.springboot.test.wiremock 146 | 147 | 148 | 149 | 150 | 151 | maven-dependency-plugin 152 | 153 | 154 | org.springframework.boot:spring-boot-starter 155 | 156 | 157 | 158 | 159 | 160 | 161 | -------------------------------------------------------------------------------- /spring-boot-wiremock/src/main/java/de/skuzzle/springboot/test/wiremock/WiremockAnnotationConfiguration.java: -------------------------------------------------------------------------------- 1 | package de.skuzzle.springboot.test.wiremock; 2 | 3 | import java.lang.reflect.AnnotatedElement; 4 | import java.util.Arrays; 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | import java.util.Objects; 8 | import java.util.Optional; 9 | import java.util.stream.Stream; 10 | 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | import org.springframework.core.annotation.MergedAnnotation; 14 | import org.springframework.core.annotation.MergedAnnotations; 15 | import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; 16 | 17 | import com.github.tomakehurst.wiremock.WireMockServer; 18 | import com.github.tomakehurst.wiremock.core.WireMockConfiguration; 19 | import com.google.common.base.Preconditions; 20 | import com.google.common.io.Resources; 21 | 22 | /** 23 | * Creates the {@link WireMockServer} from the values configured in {@link WithWiremock} 24 | * annotation. 25 | * 26 | * @author Simon Taddiken 27 | */ 28 | final class WiremockAnnotationConfiguration { 29 | 30 | private static final Logger log = LoggerFactory.getLogger(WiremockAnnotationConfiguration.class); 31 | 32 | private static final String SERVER_HTTP_HOST_PROPERTY = "wiremock.server.httpHost"; 33 | private static final String SERVER_HTTPS_HOST_PROPERTY = "wiremock.server.httpsHost"; 34 | private static final String SERVER_HTTP_PORT_PROPERTY = "wiremock.server.httpPort"; 35 | private static final String SERVER_HTTPS_PORT_PROPERTY = "wiremock.server.httpsPort"; 36 | 37 | private final WithWiremock wwm; 38 | 39 | private WiremockAnnotationConfiguration(WithWiremock wwm) { 40 | Preconditions.checkArgument(wwm != null, "WithWiremock annotation must not be null"); 41 | this.wwm = wwm; 42 | } 43 | 44 | public static WiremockAnnotationConfiguration from(WithWiremock wwm) { 45 | return new WiremockAnnotationConfiguration(wwm); 46 | } 47 | 48 | public static Optional fromAnnotatedElement(AnnotatedElement source) { 49 | return MergedAnnotations 50 | .from(source, SearchStrategy.TYPE_HIERARCHY_AND_ENCLOSING_CLASSES) 51 | .stream(WithWiremock.class) 52 | .map(MergedAnnotation::synthesize) 53 | .findFirst() 54 | .map(WiremockAnnotationConfiguration::from); 55 | } 56 | 57 | @Override 58 | public int hashCode() { 59 | return Objects.hash(wwm); 60 | } 61 | 62 | @Override 63 | public boolean equals(Object obj) { 64 | return obj == this || obj instanceof WiremockAnnotationConfiguration 65 | && Objects.equals(wwm, ((WiremockAnnotationConfiguration) obj).wwm); 66 | } 67 | 68 | public WithWiremock annotation() { 69 | return this.wwm; 70 | } 71 | 72 | public Stream getInjectHttpHostPropertyNames() { 73 | return Stream.concat(Arrays.stream(wwm.injectHttpHostInto()), Stream.of(SERVER_HTTP_HOST_PROPERTY)); 74 | } 75 | 76 | public Stream getInjectHttpsHostPropertyNames() { 77 | return Stream.concat(Arrays.stream(wwm.injectHttpsHostInto()), Stream.of(SERVER_HTTPS_HOST_PROPERTY)); 78 | } 79 | 80 | private int httpPort() { 81 | // NOTE: for HTTP (in contrast to HTTPS), the fixed port takes precedence over 82 | // random port. (Otherwise one would need to specify both randomHttpPort = false 83 | // AND fixedHttpPort = 1337 to use a fixed port (because randomHttpPort defaults 84 | // to true) 85 | if (wwm.fixedHttpPort() != WithWiremock.DEFAULT_HTTP_PORT) { 86 | return wwm.fixedHttpPort(); 87 | } 88 | if (wwm.randomHttpPort()) { 89 | Preconditions.checkArgument(wwm.fixedHttpPort() == 0, 90 | "Inconsistent HTTP port configuration. Either configure 'randomHttpPort' OR 'fixedHttpPort'"); 91 | return 0; 92 | } 93 | return wwm.fixedHttpPort(); 94 | } 95 | 96 | private int httpsPort() { 97 | if (wwm.randomHttpsPort()) { 98 | Preconditions.checkArgument(wwm.fixedHttpsPort() == WithWiremock.DEFAULT_HTTPS_PORT, 99 | "Inconsistent HTTPS port configuration. Either configure 'randomHttpsPort' OR 'fixedHttpsPort'"); 100 | return 0; 101 | } 102 | return wwm.fixedHttpsPort(); 103 | } 104 | 105 | public WireMockServer createWireMockServer() { 106 | return new WireMockServer(createWiremockConfig()); 107 | } 108 | 109 | public Map determineInjectionPropertiesFrom(WireMockServer wiremockServer) { 110 | final boolean isHttpEnabled = !wiremockServer.getOptions().getHttpDisabled(); 111 | final boolean isHttpsEnabled = wiremockServer.getOptions().httpsSettings().enabled(); 112 | final boolean sslOnly = wwm.sslOnly(); 113 | Preconditions.checkArgument(isHttpsEnabled || !sslOnly, 114 | "WireMock configured for 'sslOnly' but with HTTPS disabled. Configure httpsPort with value >= 0"); 115 | Preconditions.checkArgument(isHttpEnabled || isHttpsEnabled, 116 | "WireMock configured with disabled HTTP and disabled HTTPS. Please configure either httpPort or httpsPort with a value >= 0"); 117 | 118 | final Map props = new HashMap<>(); 119 | if (isHttpEnabled) { 120 | final String httpHost = String.format("http://localhost:%d", wiremockServer.port()); 121 | getInjectHttpHostPropertyNames() 122 | .forEach(propertyName -> props.put(propertyName, httpHost)); 123 | props.put(SERVER_HTTP_PORT_PROPERTY, "" + wiremockServer.port()); 124 | } 125 | 126 | if (isHttpsEnabled) { 127 | final String httpsHost = String.format("https://localhost:%d", wiremockServer.httpsPort()); 128 | getInjectHttpsHostPropertyNames() 129 | .forEach(propertyName -> props.put(propertyName, httpsHost)); 130 | props.put(SERVER_HTTPS_PORT_PROPERTY, "" + wiremockServer.httpsPort()); 131 | } 132 | return props; 133 | } 134 | 135 | private WireMockConfiguration createWiremockConfig() { 136 | final boolean needClientAuth = wwm.needClientAuth(); 137 | final boolean sslOnly = wwm.sslOnly(); 138 | final int httpPort = httpPort(); 139 | log.debug("Determined {} as HTTP port from {}", httpPort, wwm); 140 | final int httpsPort = httpsPort(); 141 | log.debug("Determined {} as HTTPS port from {}", httpsPort, wwm); 142 | 143 | final String keystoreLocation = getResource(wwm.keystoreLocation()); 144 | final String keystorePassword = wwm.keystorePassword(); 145 | final String keystoreType = wwm.keystoreType(); 146 | 147 | final String truststoreLocation = getResource(wwm.truststoreLocation()); 148 | final String truststorePassword = wwm.truststorePassword(); 149 | final String truststoreType = wwm.truststoreType(); 150 | 151 | final WireMockConfiguration configuration = new WireMockConfiguration() 152 | .needClientAuth(needClientAuth) 153 | .httpDisabled(sslOnly) 154 | .port(httpPort) 155 | .httpsPort(httpsPort); 156 | if (keystoreLocation != null) { 157 | configuration 158 | .keystorePath(keystoreLocation) 159 | .keystorePassword(keystorePassword) 160 | .keystoreType(keystoreType); 161 | } 162 | if (truststoreLocation != null) { 163 | configuration 164 | .trustStorePath(truststoreLocation) 165 | .trustStorePassword(truststorePassword) 166 | .trustStoreType(truststoreType); 167 | } 168 | return configuration; 169 | } 170 | 171 | private String getResource(String location) { 172 | if (location.isEmpty()) { 173 | return null; 174 | } 175 | return Resources.getResource(location).toString(); 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /spring-boot-wiremock/src/main/java/de/skuzzle/springboot/test/wiremock/StubTranslator.java: -------------------------------------------------------------------------------- 1 | package de.skuzzle.springboot.test.wiremock; 2 | 3 | import java.util.Arrays; 4 | import java.util.Iterator; 5 | import java.util.function.BiConsumer; 6 | 7 | import com.github.tomakehurst.wiremock.WireMockServer; 8 | import com.github.tomakehurst.wiremock.client.MappingBuilder; 9 | import com.github.tomakehurst.wiremock.client.ResponseDefinitionBuilder; 10 | import com.github.tomakehurst.wiremock.client.WireMock; 11 | import com.github.tomakehurst.wiremock.matching.StringValuePattern; 12 | import com.github.tomakehurst.wiremock.matching.UrlPattern; 13 | import com.google.common.base.Preconditions; 14 | 15 | import de.skuzzle.springboot.test.wiremock.stubs.Auth; 16 | import de.skuzzle.springboot.test.wiremock.stubs.HttpStub; 17 | import de.skuzzle.springboot.test.wiremock.stubs.Request; 18 | import de.skuzzle.springboot.test.wiremock.stubs.Response; 19 | import de.skuzzle.springboot.test.wiremock.stubs.Scenario; 20 | import de.skuzzle.springboot.test.wiremock.stubs.WrapAround; 21 | 22 | /** 23 | * Translates the {@link HttpStub} instance into a WireMock stub. 24 | * 25 | * @author Simon Taddiken 26 | */ 27 | class StubTranslator { 28 | 29 | static void configureStubOn(WireMockServer wiremock, WithWiremock withWiremock, HttpStub stub) { 30 | final boolean multipleResponseStubs = stub.respond().length > 1; 31 | Preconditions.checkArgument(!multipleResponseStubs || 32 | nullIfEmpty(stub.onRequest().scenario().name()) == null, 33 | "Scenario not supported within stub with multiple responses"); 34 | 35 | final Iterator responses = Arrays.asList(stub.respond()).iterator(); 36 | 37 | final WrapAround wrapAround = stub.onLastResponse(); 38 | int state = 0; 39 | while (responses.hasNext()) { 40 | final Response response = responses.next(); 41 | 42 | final MappingBuilder requestBuilder = buildRequest(withWiremock, stub.onRequest()); 43 | 44 | if (multipleResponseStubs) { 45 | final String scenarioName = stub.toString(); 46 | 47 | final int nextState = wrapAround.determineNextState(state, responses.hasNext()); 48 | 49 | requestBuilder.inScenario(scenarioName) 50 | .whenScenarioStateIs(translateState(state)) 51 | .willSetStateTo(translateState(nextState)); 52 | } 53 | 54 | final ResponseDefinitionBuilder responseBuilder = buildResponse(response); 55 | wiremock.stubFor(requestBuilder.willReturn(responseBuilder)); 56 | ++state; 57 | } 58 | } 59 | 60 | private static String translateState(int state) { 61 | return state == 0 62 | ? com.github.tomakehurst.wiremock.stubbing.Scenario.STARTED 63 | : "" + state; 64 | } 65 | 66 | private static MappingBuilder buildRequest(WithWiremock withWiremock, Request request) { 67 | final String toUrl = nullIfEmpty(request.toUrl()); 68 | final String toUrlPattern = nullIfEmpty(request.toUrlPattern()); 69 | final String toUrlPath = nullIfEmpty(request.toUrlPath()); 70 | final String toUrlPathPattern = nullIfEmpty(request.toUrlPathPattern()); 71 | mutuallyExclusive( 72 | parameters("url", "urlPattern", "urlPath", "urlPathPattern"), 73 | values(toUrl, toUrlPattern, toUrlPath, toUrlPathPattern)); 74 | final UrlPattern urlPattern = UrlPattern.fromOneOf(toUrl, toUrlPattern, toUrlPath, toUrlPathPattern); 75 | 76 | final MappingBuilder requestBuilder = WireMock.request(request.withMethod(), urlPattern); 77 | 78 | final Scenario scenario = request.scenario(); 79 | final String scenarioName = nullIfEmpty(scenario.name()); 80 | if (scenarioName != null) { 81 | final String scenarioState = nullIfEmpty(scenario.state()); 82 | final String nextState = defaultIfEmpty(scenario.nextState(), scenarioName); 83 | requestBuilder.inScenario(scenarioName) 84 | .whenScenarioStateIs(scenarioState) 85 | .willSetStateTo(nextState); 86 | } 87 | 88 | parseValueArray(request.containingHeaders(), requestBuilder::withHeader); 89 | parseValueArray(request.withQueryParameters(), requestBuilder::withQueryParam); 90 | parseValueArray(request.containingCookies(), requestBuilder::withCookie); 91 | 92 | if (!configureAuthentication(requestBuilder, request.authenticatedBy())) { 93 | configureAuthentication(requestBuilder, withWiremock.withGlobalAuthentication()); 94 | } 95 | 96 | final String requestBody = nullIfEmpty(request.withBody()); 97 | if (requestBody != null) { 98 | requestBuilder.withRequestBody(StringValuePatterns.parseFromPrefix(requestBody)); 99 | } 100 | 101 | final int priority = request.priority(); 102 | if (priority != Request.NO_PRIORITY) { 103 | requestBuilder.atPriority(priority); 104 | } 105 | 106 | return requestBuilder; 107 | } 108 | 109 | private static boolean configureAuthentication(MappingBuilder mappingBuilder, Auth authenticatedBy) { 110 | final String basicAuthUsername = nullIfEmpty(authenticatedBy.basicAuthUsername()); 111 | final String basicAuthPassword = nullIfEmpty(authenticatedBy.basicAuthPassword()); 112 | final String bearerToken = nullIfEmpty(authenticatedBy.bearerToken()); 113 | mutuallyExclusive(parameters("basicAuthPassword", "bearerToken"), values(basicAuthPassword, bearerToken)); 114 | mutuallyExclusive(parameters("basicAuthUsername", "bearerToken"), values(basicAuthUsername, bearerToken)); 115 | 116 | if (basicAuthUsername != null && basicAuthPassword != null) { 117 | mappingBuilder.withBasicAuth(basicAuthUsername, basicAuthPassword); 118 | return true; 119 | } else if (bearerToken != null) { 120 | mappingBuilder.withHeader("Authorization", WireMock.equalToIgnoreCase("Bearer " + bearerToken)); 121 | return true; 122 | } 123 | 124 | return false; 125 | } 126 | 127 | private static ResponseDefinitionBuilder buildResponse(Response response) { 128 | final ResponseDefinitionBuilder responseBuilder = WireMock.aResponse() 129 | .withStatus(response.withStatus().value()); 130 | 131 | final String statusMessage = nullIfEmpty(response.withStatusMessage()); 132 | if (statusMessage != null) { 133 | responseBuilder.withStatusMessage(statusMessage); 134 | } 135 | 136 | final String body = nullIfEmpty(response.withBody()); 137 | final String jsonBody = nullIfEmpty(response.withJsonBody()); 138 | final String bodyBase64 = nullIfEmpty(response.withBodyBase64()); 139 | final String bodyFile = nullIfEmpty(response.withBodyFile()); 140 | mutuallyExclusive( 141 | parameters("body", "bodyBase64", "bodyFile", "jsonBody"), 142 | values(body, bodyBase64, bodyFile, jsonBody)); 143 | 144 | if (body != null) { 145 | responseBuilder.withBody(body); 146 | } else if (bodyBase64 != null) { 147 | responseBuilder.withBase64Body(bodyBase64); 148 | } else if (bodyFile != null) { 149 | responseBuilder.withBodyFile(bodyFile); 150 | } else if (jsonBody != null) { 151 | responseBuilder 152 | .withBody(jsonBody) 153 | .withHeader("Content-Type", "application/json"); 154 | } 155 | 156 | final String[] responseHeaders = response.withHeaders(); 157 | for (final String headerAndValue : responseHeaders) { 158 | final String[] parts = headerAndValue.split("=", 2); 159 | responseBuilder.withHeader(parts[0], parts[1]); 160 | } 161 | 162 | final String responseContentType = nullIfEmpty(response.withContentType()); 163 | if (responseContentType != null) { 164 | responseBuilder.withHeader("Content-Type", responseContentType); 165 | } 166 | return responseBuilder; 167 | } 168 | 169 | private static void parseValueArray(String[] array, BiConsumer builder) { 170 | for (final String keyAndValue : array) { 171 | final String[] parts = keyAndValue.split("=", 2); 172 | final String key = parts[0]; 173 | final String valueWithPrefix = parts[1]; 174 | final StringValuePattern valuePattern = StringValuePatterns.parseFromPrefix(valueWithPrefix); 175 | builder.accept(key, valuePattern); 176 | } 177 | } 178 | 179 | private static String defaultIfEmpty(String s, String defaultValue) { 180 | return s == null || s.isEmpty() ? defaultValue : s; 181 | } 182 | 183 | private static String nullIfEmpty(String s) { 184 | return s == null || s.isEmpty() ? null : s; 185 | } 186 | 187 | private static void mutuallyExclusive(String[] names, Object[] args) { 188 | final long notNullCount = Arrays.stream(args).filter(arg -> arg != null).count(); 189 | Preconditions.checkArgument(notNullCount <= 1, 190 | "Parameters '%s' are mutually exclusive and only one must be specified", Arrays.toString(names)); 191 | } 192 | 193 | private static String[] parameters(String... names) { 194 | return names; 195 | } 196 | 197 | private static Object[] values(Object... values) { 198 | return values; 199 | } 200 | 201 | } 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [![Maven Central](https://img.shields.io/static/v1?label=MavenCentral&message=0.0.18&color=blue)](https://search.maven.org/artifact/de.skuzzle.springboot.test/spring-boot-wiremock/0.0.18/jar) 4 | [![JavaDoc](https://img.shields.io/static/v1?label=JavaDoc&message=0.0.18&color=orange)](http://www.javadoc.io/doc/de.skuzzle.springboot.test/spring-boot-wiremock/0.0.18) 5 | [![Coverage Status](https://coveralls.io/repos/github/skuzzle/spring-boot-wiremock/badge.svg?branch=main)](https://coveralls.io/github/skuzzle/spring-boot-wiremock?branch=main) 6 | [![Twitter Follow](https://img.shields.io/twitter/follow/skuzzleOSS.svg?style=social)](https://twitter.com/skuzzleOSS) 7 | 8 | # spring-boot-wiremock 9 | _This is **not** an official extension from the Spring Team!_ (Though one exists as part of the 10 | [spring-cloud](https://cloud.spring.io/spring-cloud-contract/reference/html/project-features.html#features-wiremock) 11 | project). 12 | 13 | _The easiest way to setup a [WireMock](http://wiremock.org/) server in your Spring-Boot tests._ 14 | - [x] Run WireMock server on random port 15 | - [x] Inject WireMock hosts (http and https) as spring application property 16 | - [x] Easily setup server- and client side SSL 17 | - [x] Declarative stubs using annotations 18 | 19 | ```xml 20 | 21 | de.skuzzle.springboot.test 22 | spring-boot-wiremock 23 | 0.0.18 24 | test 25 | 26 | ``` 27 | 28 | ``` 29 | testImplementation 'de.skuzzle.springboot.test:spring-boot-wiremock:0.0.18' 30 | ``` 31 | 32 | ## Quick start 33 | All you need to do is to add the `@WithWiremock` annotation to your Spring-Boot test. The annotation has some 34 | configuration options but the most notable one is `injectHttpHostInto`. 35 | 36 | ```java 37 | @SpringBootTest 38 | @WithWiremock(injectHttpHostInto = "your.application.serviceUrl") 39 | public class WiremockTest { 40 | 41 | @Value("${your.application.serviceUrl}") 42 | private String serviceUrl; 43 | @Autowired 44 | private WireMockServer wiremock; 45 | 46 | @Test 47 | void testWithExplicitStub() throws Exception { 48 | // Use standard WireMock API for minimum coupling to this library 49 | wiremock.stubFor(WireMock.get("/") 50 | .willReturn(aResponse().withStatus(200))); 51 | 52 | final ResponseEntity response = new RestTemplate() 53 | .getForEntity(serviceUrl, Object.class); 54 | assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); 55 | } 56 | 57 | @Test 58 | @HttpStub( 59 | onRequest = @Request(withMethod = "GET"), 60 | respond = @Response(withStatus = HttpStatus.OK) 61 | ) 62 | void testWithAnnotationStub() throws Exception { 63 | // Make full use of this library by defining stubs using annotations 64 | 65 | final ResponseEntity response = new RestTemplate() 66 | .getForEntity(serviceUrl, Object.class); 67 | assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); 68 | } 69 | } 70 | ``` 71 | Injecting the host into the application context happens _before_ any bean instances are created and the injected 72 | property values takes precedence over any other, for example statically configured value. This means, in most cases the 73 | extension works out of the box with your current context configuration. 74 | 75 | You can see more annotation stubbing examples in 76 | [this](https://github.com/skuzzle/spring-boot-wiremock/blob/main/src/test/java/de/skuzzle/springboot/test/wiremock/TestHttpStub.java) 77 | test class. 78 | 79 | ## Rationale 80 | [WireMock](http://wiremock.org/) is an awesome library for mocking HTTP endpoints in unit tests. However, it can be 81 | quite cumbersome to integrate with Spring-Boot: when you manually manage the `WireMockServer` from within the test, 82 | there is hardly a chance to use its random base url during Bean creation. That often results in the weirdest stunts of 83 | Spring context configuration in order to somehow inject the mock location. For example, your client under test might 84 | use the `RestTemplate` and you decide to make it a mutable field in order to replace it in your test with an instance 85 | that knows the WireMock's location. 86 | 87 | In a perfect world, you would not need to touch your existing context configuration for just injecting a mock. Consider 88 | the `@MockBean` annotation that allows to simply replace an already configured Bean with a mock. This works without a 89 | hassle and involves no stunts like defining a new Bean with same type and `@Primary` annotation or manually replacing 90 | an injected instance using a setter. 91 | 92 | The `@WithWiremock` annotation works just like that: It sets up a WireMock server early enough, so that its base url 93 | can be injected into the Spring application properties, simply replacing an existing value. 94 | 95 | ## Compatibility 96 | - [x] Requires Java 11 97 | - [x] Tested against Spring-Boot `2.2.13.RELEASE, 2.3.12.RELEASE, 2.4.11, 2.5.5` 98 | - [x] Tested against WireMock `2.27.2` 99 | 100 | ## Usage 101 | 102 | ### WireMock based stubbing 103 | If you set up WireMock using `@WithWireMock` the server instance is made available as bean in the Spring 104 | `ApplicationContext`. It can thus be injected into the test class like this: 105 | 106 | ```java 107 | @WithWiremock(...) 108 | @SpringBootTest 109 | public class WiremockTest { 110 | 111 | @Autowired 112 | private WireMockServer wiremock; 113 | } 114 | ``` 115 | 116 | You can then use the normal WireMock API in your test methods to define stubs and verifications. 117 | 118 | ### Annotation based stubbing 119 | If you opt-in to use annotation based stubbing provided by this library you gain the advantages of full declarative 120 | stubbing and easily reusable stubs. 121 | 122 | > **Warning**: Please note that using annotation based stubbing will make it harder to get rid of this library from your 123 | > code base in the future. You should consider to only use WireMock based stubbing to reduce coupling to this library. 124 | 125 | Not all WireMock features (e.g. verifications) are available in annotation based stubbing. It is always possible though 126 | to combine annotation based stubs with plain WireMock based stubs as describe above. 127 | 128 | #### Simple stubs 129 | You can define a simple stub by annotating your test/test class with `@HttpStub`. If you specify no further attributes, 130 | the mock will now respond with `200 OK` for every request it receives. Note that all additional attributes are optional. 131 | 132 | Here is a more sophisticated stub example: 133 | ```java 134 | @HttpStub( 135 | onRequest = @Request( 136 | withMethod = "POST", 137 | toUrlPath = "/endpoint", 138 | withQueryParameters = "param=matching:[a-z]+", 139 | containingHeaders = "Request-Header=eq:value", 140 | containingCookies = "sessionId=containing:123456", 141 | withBody = "containing:Just a body", 142 | authenticatedBy = @Auth( 143 | basicAuthUsername = "username", 144 | basicAuthPassword = "password")), 145 | respond = @Response( 146 | withStatus = HttpStatus.CREATED, 147 | withBody = "Hello World", 148 | withContentType = "application/text", 149 | withHeaders = "Response-Header=value")) 150 | ``` 151 | 152 | #### String matching 153 | All stub request attributes that expect a String value optionally take a matcher prefix like shown in the above example. 154 | The following prefixes are supported: 155 | | Prefix | Operation | 156 | |--------------------|-----------| 157 | |`eq:` | Comparison using `String.equals` | 158 | |`eqIgnoreCase:` | Comparison using `String.equalsIgnoreCase` | 159 | |`eqToJson:` | Interpretes the strings as json | 160 | |`eqToXml` | Interpretes the strings as xml | 161 | |`matching:` | Comparison using the provided regex pattern | 162 | |`notMatching:` | Comparison using the provided regex pattern but negates the result | 163 | |`matchingJsonPath:` | Interpretes the string as json and matches it against the provided json path | 164 | |`matchingXPath:` | Interpretes the string as xml and matches it against the provided xpath | 165 | |`containing:` | Comparison using `String.contains` | 166 | 167 | No prefix always results in a comparison using `String.equals`. 168 | 169 | #### Multiple responses 170 | It is possible to define multiple responses that will be returned by the stub when a stub is matched by consecutive 171 | requests. Internally this feature will create a WireMock scenario, thus you can not combine multiple responses and 172 | explicit scenario creation using `Request.scenario`. 173 | 174 | ```java 175 | @HttpStub( 176 | respond = { 177 | @Response(withStatus = HttpStatus.CREATED), 178 | @Response(withStatus = HttpStatus.OK), 179 | @Response(withStatus = HttpStatus.ACCEPTED) 180 | }) 181 | ``` 182 | When stubbing multiple responses you can define what happens when the last response has been returned using 183 | `HttpStub.onLastResponse` with the following options: 184 | 185 | | `onLastResponse` | Behavior | 186 | |--------------------------|----------| 187 | |`WrapAround.RETURN_ERROR` | Default behavior. Mock will answer with a `403` code after the last stubbed response | 188 | |`WrapAround.START_OVER` | After the last response the mock will start over and answer with the first stubbed response | 189 | |`WrapAround.REPEAT` | The mock keeps returning the last stubbed response | 190 | 191 | ```java 192 | @HttpStub( 193 | // ... 194 | respond = { 195 | // ... 196 | }, 197 | onLastResponse = WrapAround.REPEAT; 198 | ) 199 | ``` 200 | 201 | #### Sharing stubs 202 | It is possible to share stubs among multiple tests. You can either define your stubs on a super class or an interface 203 | implemented by your test class. However, the preferred way of sharing stubs is to create a new annotation which 204 | is meta-annotated with all the stubs (and optionally also with `@WithWiremock`) like in the following example: 205 | 206 | ```java 207 | @Retention(RUNTIME) 208 | @Target(TYPE) 209 | @WithWiremock(injectHttpHostInto = "sample-service.url") 210 | @HttpStub(onRequest = @Request(toUrl = "/info"), 211 | respond = @Response(withStatus = HttpStatus.OK, withStatusMessage = "Everything is Ok")) 212 | @HttpStub(onRequest = @Request(toUrl = "/submit/entity", withMethod = "PUT"), respond = { 213 | @Response(withStatus = HttpStatus.CREATED, withStatusMessage = "Entity created"), 214 | @Response(withStatus = HttpStatus.OK, withStatusMessage = "Entity already exists") 215 | }) 216 | public @interface WithSampleServiceMock { 217 | 218 | } 219 | ``` 220 | 221 | You can now easily reuse the complete mock definition in any `SpringBootTest`: 222 | ```java 223 | @SpringBootTest 224 | @WithSampleServiceMock 225 | public class MetaAnnotatedTest { 226 | 227 | @Value("${sample-service.url}") 228 | private String sampleServiceUrl; 229 | 230 | // ... 231 | } 232 | ``` 233 | 234 | -------------------------------------------------------------------------------- /readme/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [![Maven Central](https://img.shields.io/static/v1?label=MavenCentral&message=${project.version}&color=blue)](https://search.maven.org/artifact/${project.groupId}/${project.artifactId}/${project.version}/jar) 4 | [![JavaDoc](https://img.shields.io/static/v1?label=JavaDoc&message=${project.version}&color=orange)](http://www.javadoc.io/doc/${project.groupId}/${project.artifactId}/${project.version}) 5 | [![Coverage Status](https://coveralls.io/repos/github/skuzzle/${github.name}/badge.svg?branch=main)](https://coveralls.io/github/skuzzle/${github.name}?branch=main) 6 | [![Twitter Follow](https://img.shields.io/twitter/follow/skuzzleOSS.svg?style=social)](https://twitter.com/skuzzleOSS) 7 | 8 | # spring-boot-wiremock 9 | _This is **not** an official extension from the Spring Team!_ (Though one exists as part of the 10 | [spring-cloud](https://cloud.spring.io/spring-cloud-contract/reference/html/project-features.html#features-wiremock) 11 | project). 12 | 13 | _The easiest way to setup a [WireMock](http://wiremock.org/) server in your Spring-Boot tests._ 14 | - [x] Run WireMock server on random port 15 | - [x] Inject WireMock hosts (http and https) as spring application property 16 | - [x] Easily setup server- and client side SSL 17 | - [x] Declarative stubs using annotations 18 | 19 | ```xml 20 | 21 | ${project.groupId} 22 | ${project.artifactId} 23 | ${project.version} 24 | test 25 | 26 | ``` 27 | 28 | ``` 29 | testImplementation '${project.groupId}:${project.artifactId}:${project.version}' 30 | ``` 31 | 32 | ## Quick start 33 | All you need to do is to add the `@WithWiremock` annotation to your Spring-Boot test. The annotation has some 34 | configuration options but the most notable one is `injectHttpHostInto`. 35 | 36 | ```java 37 | @SpringBootTest 38 | @WithWiremock(injectHttpHostInto = "your.application.serviceUrl") 39 | public class WiremockTest { 40 | 41 | @Value("${your.application.serviceUrl}") 42 | private String serviceUrl; 43 | @Autowired 44 | private WireMockServer wiremock; 45 | 46 | @Test 47 | void testWithExplicitStub() throws Exception { 48 | // Use standard WireMock API for minimum coupling to this library 49 | wiremock.stubFor(WireMock.get("/") 50 | .willReturn(aResponse().withStatus(200))); 51 | 52 | final ResponseEntity response = new RestTemplate() 53 | .getForEntity(serviceUrl, Object.class); 54 | assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); 55 | } 56 | 57 | @Test 58 | @HttpStub( 59 | onRequest = @Request(withMethod = "GET"), 60 | respond = @Response(withStatus = HttpStatus.OK) 61 | ) 62 | void testWithAnnotationStub() throws Exception { 63 | // Make full use of this library by defining stubs using annotations 64 | 65 | final ResponseEntity response = new RestTemplate() 66 | .getForEntity(serviceUrl, Object.class); 67 | assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); 68 | } 69 | } 70 | ``` 71 | Injecting the host into the application context happens _before_ any bean instances are created and the injected 72 | property values takes precedence over any other, for example statically configured value. This means, in most cases the 73 | extension works out of the box with your current context configuration. 74 | 75 | You can see more annotation stubbing examples in 76 | [this](https://github.com/skuzzle/spring-boot-wiremock/blob/main/src/test/java/de/skuzzle/springboot/test/wiremock/TestHttpStub.java) 77 | test class. 78 | 79 | ## Rationale 80 | [WireMock](http://wiremock.org/) is an awesome library for mocking HTTP endpoints in unit tests. However, it can be 81 | quite cumbersome to integrate with Spring-Boot: when you manually manage the `WireMockServer` from within the test, 82 | there is hardly a chance to use its random base url during Bean creation. That often results in the weirdest stunts of 83 | Spring context configuration in order to somehow inject the mock location. For example, your client under test might 84 | use the `RestTemplate` and you decide to make it a mutable field in order to replace it in your test with an instance 85 | that knows the WireMock's location. 86 | 87 | In a perfect world, you would not need to touch your existing context configuration for just injecting a mock. Consider 88 | the `@MockBean` annotation that allows to simply replace an already configured Bean with a mock. This works without a 89 | hassle and involves no stunts like defining a new Bean with same type and `@Primary` annotation or manually replacing 90 | an injected instance using a setter. 91 | 92 | The `@WithWiremock` annotation works just like that: It sets up a WireMock server early enough, so that its base url 93 | can be injected into the Spring application properties, simply replacing an existing value. 94 | 95 | ## Compatibility 96 | - [x] Requires Java 11 97 | - [x] Tested against Spring-Boot `${version.spring-boot}, ${compatible-spring-boot-versions}` 98 | - [x] Tested against WireMock `${version.wiremock}` 99 | 100 | ## Usage 101 | 102 | ### WireMock based stubbing 103 | If you set up WireMock using `@WithWireMock` the server instance is made available as bean in the Spring 104 | `ApplicationContext`. It can thus be injected into the test class like this: 105 | 106 | ```java 107 | @WithWiremock(...) 108 | @SpringBootTest 109 | public class WiremockTest { 110 | 111 | @Autowired 112 | private WireMockServer wiremock; 113 | } 114 | ``` 115 | 116 | You can then use the normal WireMock API in your test methods to define stubs and verifications. 117 | 118 | ### Annotation based stubbing 119 | If you opt-in to use annotation based stubbing provided by this library you gain the advantages of full declarative 120 | stubbing and easily reusable stubs. 121 | 122 | > **Warning**: Please note that using annotation based stubbing will make it harder to get rid of this library from your 123 | > code base in the future. You should consider to only use WireMock based stubbing to reduce coupling to this library. 124 | 125 | Not all WireMock features (e.g. verifications) are available in annotation based stubbing. It is always possible though 126 | to combine annotation based stubs with plain WireMock based stubs as describe above. 127 | 128 | #### Simple stubs 129 | You can define a simple stub by annotating your test/test class with `@HttpStub`. If you specify no further attributes, 130 | the mock will now respond with `200 OK` for every request it receives. Note that all additional attributes are optional. 131 | 132 | Here is a more sophisticated stub example: 133 | ```java 134 | @HttpStub( 135 | onRequest = @Request( 136 | withMethod = "POST", 137 | toUrlPath = "/endpoint", 138 | withQueryParameters = "param=matching:[a-z]+", 139 | containingHeaders = "Request-Header=eq:value", 140 | containingCookies = "sessionId=containing:123456", 141 | withBody = "containing:Just a body", 142 | authenticatedBy = @Auth( 143 | basicAuthUsername = "username", 144 | basicAuthPassword = "password")), 145 | respond = @Response( 146 | withStatus = HttpStatus.CREATED, 147 | withBody = "Hello World", 148 | withContentType = "application/text", 149 | withHeaders = "Response-Header=value")) 150 | ``` 151 | 152 | #### String matching 153 | All stub request attributes that expect a String value optionally take a matcher prefix like shown in the above example. 154 | The following prefixes are supported: 155 | | Prefix | Operation | 156 | |--------------------|-----------| 157 | |`eq:` | Comparison using `String.equals` | 158 | |`eqIgnoreCase:` | Comparison using `String.equalsIgnoreCase` | 159 | |`eqToJson:` | Interpretes the strings as json | 160 | |`eqToXml` | Interpretes the strings as xml | 161 | |`matching:` | Comparison using the provided regex pattern | 162 | |`notMatching:` | Comparison using the provided regex pattern but negates the result | 163 | |`matchingJsonPath:` | Interpretes the string as json and matches it against the provided json path | 164 | |`matchingXPath:` | Interpretes the string as xml and matches it against the provided xpath | 165 | |`containing:` | Comparison using `String.contains` | 166 | 167 | No prefix always results in a comparison using `String.equals`. 168 | 169 | #### Multiple responses 170 | It is possible to define multiple responses that will be returned by the stub when a stub is matched by consecutive 171 | requests. Internally this feature will create a WireMock scenario, thus you can not combine multiple responses and 172 | explicit scenario creation using `Request.scenario`. 173 | 174 | ```java 175 | @HttpStub( 176 | respond = { 177 | @Response(withStatus = HttpStatus.CREATED), 178 | @Response(withStatus = HttpStatus.OK), 179 | @Response(withStatus = HttpStatus.ACCEPTED) 180 | }) 181 | ``` 182 | When stubbing multiple responses you can define what happens when the last response has been returned using 183 | `HttpStub.onLastResponse` with the following options: 184 | 185 | | `onLastResponse` | Behavior | 186 | |--------------------------|----------| 187 | |`WrapAround.RETURN_ERROR` | Default behavior. Mock will answer with a `403` code after the last stubbed response | 188 | |`WrapAround.START_OVER` | After the last response the mock will start over and answer with the first stubbed response | 189 | |`WrapAround.REPEAT` | The mock keeps returning the last stubbed response | 190 | 191 | ```java 192 | @HttpStub( 193 | // ... 194 | respond = { 195 | // ... 196 | }, 197 | onLastResponse = WrapAround.REPEAT; 198 | ) 199 | ``` 200 | 201 | #### Sharing stubs 202 | It is possible to share stubs among multiple tests. You can either define your stubs on a super class or an interface 203 | implemented by your test class. However, the preferred way of sharing stubs is to create a new annotation which 204 | is meta-annotated with all the stubs (and optionally also with `@WithWiremock`) like in the following example: 205 | 206 | ```java 207 | @Retention(RUNTIME) 208 | @Target(TYPE) 209 | @WithWiremock(injectHttpHostInto = "sample-service.url") 210 | @HttpStub(onRequest = @Request(toUrl = "/info"), 211 | respond = @Response(withStatus = HttpStatus.OK, withStatusMessage = "Everything is Ok")) 212 | @HttpStub(onRequest = @Request(toUrl = "/submit/entity", withMethod = "PUT"), respond = { 213 | @Response(withStatus = HttpStatus.CREATED, withStatusMessage = "Entity created"), 214 | @Response(withStatus = HttpStatus.OK, withStatusMessage = "Entity already exists") 215 | }) 216 | public @interface WithSampleServiceMock { 217 | 218 | } 219 | ``` 220 | 221 | You can now easily reuse the complete mock definition in any `SpringBootTest`: 222 | ```java 223 | @SpringBootTest 224 | @WithSampleServiceMock 225 | public class MetaAnnotatedTest { 226 | 227 | @Value("${sample-service.url}") 228 | private String sampleServiceUrl; 229 | 230 | // ... 231 | } 232 | ``` 233 | 234 | -------------------------------------------------------------------------------- /spring-boot-wiremock/src/test/java/de/skuzzle/springboot/test/wiremock/TestHttpStub.java: -------------------------------------------------------------------------------- 1 | package de.skuzzle.springboot.test.wiremock; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | import static org.assertj.core.api.Assertions.assertThatExceptionOfType; 5 | 6 | import java.net.URI; 7 | 8 | import org.junit.jupiter.api.Test; 9 | import org.springframework.beans.factory.annotation.Value; 10 | import org.springframework.boot.test.context.SpringBootTest; 11 | import org.springframework.boot.web.client.RestTemplateBuilder; 12 | import org.springframework.http.HttpStatus; 13 | import org.springframework.http.RequestEntity; 14 | import org.springframework.http.ResponseEntity; 15 | import org.springframework.web.client.HttpClientErrorException; 16 | import org.springframework.web.client.RestTemplate; 17 | 18 | import de.skuzzle.springboot.test.wiremock.client.TestClients; 19 | import de.skuzzle.springboot.test.wiremock.client.TestClients.ClientBuilder; 20 | import de.skuzzle.springboot.test.wiremock.stubs.Auth; 21 | import de.skuzzle.springboot.test.wiremock.stubs.HttpStub; 22 | import de.skuzzle.springboot.test.wiremock.stubs.Request; 23 | import de.skuzzle.springboot.test.wiremock.stubs.Response; 24 | import de.skuzzle.springboot.test.wiremock.stubs.Scenario; 25 | import de.skuzzle.springboot.test.wiremock.stubs.WrapAround; 26 | 27 | @SpringBootTest 28 | @TestStubCollectionAnnotation 29 | @WithWiremock(injectHttpHostInto = "mockHost") 30 | public class TestHttpStub implements TestStubCollectionInterface { 31 | 32 | @Value("${mockHost}") 33 | private String mockHost; 34 | 35 | private ClientBuilder client() { 36 | return TestClients.restTemplate() 37 | .withBaseUrl(mockHost); 38 | } 39 | 40 | private URI url(String path) { 41 | return URI.create(mockHost + path); 42 | } 43 | 44 | @Test 45 | @HttpStub(respond = @Response(withStatus = HttpStatus.CREATED)) 46 | void testMatchAnyUrl() throws Exception { 47 | final ResponseEntity response = client().build().getForEntity(url("/whatever"), Object.class); 48 | assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED); 49 | } 50 | 51 | @Test 52 | @HttpStub( 53 | onRequest = @Request( 54 | withMethod = "POST", 55 | toUrlPath = "/endpoint", 56 | withQueryParameters = "param=matching:[a-z]+", 57 | containingHeaders = "Request-Header=eq:value", 58 | containingCookies = "sessionId=containing:123456", 59 | withBody = "containing:Just a body", 60 | authenticatedBy = @Auth( 61 | basicAuthUsername = "username", 62 | basicAuthPassword = "password")), 63 | respond = @Response( 64 | withStatus = HttpStatus.CREATED, 65 | withBody = "Hello World", 66 | withContentType = "application/text", 67 | withHeaders = "Response-Header=value")) 68 | void testSimpleStubWithBasicAuth_Body_ContentType_Cookie_QueryParam_And_Headers() { 69 | final RequestEntity request = RequestEntity.post(url("/endpoint?param=abc")) 70 | .header("Request-Header", "value") 71 | .header("Cookie", "sessionId=1234567890") 72 | .body("Just a body"); 73 | final ResponseEntity response = client() 74 | .withBasicAuth("username", "password") 75 | .build() 76 | .exchange(request, String.class); 77 | assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED); 78 | assertThat(response.getBody()).isEqualTo("Hello World"); 79 | assertThat(response.getHeaders().get("Content-Type")).containsOnly("application/text"); 80 | assertThat(response.getHeaders().get("Response-Header")).containsOnly("value"); 81 | } 82 | 83 | @Test 84 | @HttpStub(onRequest = @Request(authenticatedBy = @Auth(bearerToken = "valid-token"))) 85 | void testBearerAuth() { 86 | final RequestEntity requestEntity = RequestEntity.get(url("/")) 87 | .header("Authorization", "bearer Valid-Token") 88 | .build(); 89 | final ResponseEntity response = client().build().exchange(requestEntity, String.class); 90 | assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); 91 | } 92 | 93 | @Test 94 | @HttpStub( 95 | onRequest = @Request( 96 | withMethod = "POST", 97 | toUrl = "/endpoint"), 98 | respond = @Response( 99 | withStatus = HttpStatus.CREATED, 100 | withBody = "Hello World", 101 | withContentType = "application/text", 102 | withHeaders = "Content-Type=application/json")) 103 | void testContenTypeTakesPrecedenceOverHeaders() { 104 | final ResponseEntity response = client().build().postForEntity(url("/endpoint"), null, String.class); 105 | assertThat(response.getBody()).isEqualTo("Hello World"); 106 | assertThat(response.getHeaders().get("Content-Type")).containsOnly("application/text"); 107 | } 108 | 109 | @Test 110 | @HttpStub 111 | @HttpStub(onRequest = @Request(withMethod = "POST"), respond = @Response(withStatus = HttpStatus.CREATED)) 112 | void testMultipleStubs() throws Exception { 113 | final ResponseEntity responseGet = client().build().getForEntity("/", null, String.class); 114 | assertThat(responseGet.getStatusCode()).isEqualTo(HttpStatus.OK); 115 | 116 | final ResponseEntity responsePost = client().build().postForEntity("/", null, String.class); 117 | assertThat(responsePost.getStatusCode()).isEqualTo(HttpStatus.CREATED); 118 | } 119 | 120 | @Test 121 | @HttpStub(onRequest = @Request(scenario = @Scenario(name = "Scenario", nextState = "1"))) 122 | @HttpStub(onRequest = @Request(scenario = @Scenario(name = "Scenario", state = "1", nextState = "2")), 123 | respond = @Response(withStatus = HttpStatus.CREATED)) 124 | @HttpStub(onRequest = @Request(scenario = @Scenario(name = "Scenario", state = "2", nextState = "1")), 125 | respond = @Response(withStatus = HttpStatus.OK)) 126 | void testScenario() throws Exception { 127 | final ResponseEntity response1 = client().build().getForEntity(url("/"), String.class); 128 | assertThat(response1.getStatusCode()).isEqualTo(HttpStatus.OK); 129 | final ResponseEntity response2 = client().build().getForEntity(url("/"), String.class); 130 | assertThat(response2.getStatusCode()).isEqualTo(HttpStatus.CREATED); 131 | 132 | final ResponseEntity response3 = client().build().getForEntity(url("/"), String.class); 133 | assertThat(response3.getStatusCode()).isEqualTo(HttpStatus.OK); 134 | final ResponseEntity response4 = client().build().getForEntity(url("/"), String.class); 135 | assertThat(response4.getStatusCode()).isEqualTo(HttpStatus.CREATED); 136 | } 137 | 138 | @Test 139 | @HttpStub(respond = @Response(withBodyBase64 = "SGVsbG8gV29ybGQ=", withContentType = "text/plain")) 140 | void testWithBodyBase64() throws Exception { 141 | final ResponseEntity response = client().build().getForEntity(url("/"), String.class); 142 | assertThat(response.getBody()).isEqualTo("Hello World"); 143 | } 144 | 145 | @Test 146 | @HttpStub(respond = @Response(withBodyFile = "bodyFile.txt", withContentType = "text/plain")) 147 | void testWithBodyFromFile() throws Exception { 148 | final ResponseEntity response = client().build().getForEntity(url("/"), String.class); 149 | assertThat(response.getBody()).isEqualTo("Hello World"); 150 | } 151 | 152 | @Test 153 | @HttpStub( 154 | onLastResponse = WrapAround.START_OVER, 155 | respond = { 156 | @Response(withStatus = HttpStatus.CREATED), 157 | @Response(withStatus = HttpStatus.OK), 158 | @Response(withStatus = HttpStatus.ACCEPTED) 159 | }) 160 | void testConsecutiveWithtWrapAround() throws Exception { 161 | final ResponseEntity response1 = client().build().getForEntity(url("/"), String.class); 162 | assertThat(response1.getStatusCode()).isEqualTo(HttpStatus.CREATED); 163 | final ResponseEntity response2 = client().build().getForEntity(url("/"), String.class); 164 | assertThat(response2.getStatusCode()).isEqualTo(HttpStatus.OK); 165 | 166 | final ResponseEntity response3 = client().build().getForEntity(url("/"), String.class); 167 | assertThat(response3.getStatusCode()).isEqualTo(HttpStatus.ACCEPTED); 168 | 169 | final ResponseEntity response4 = client().build().getForEntity(url("/"), String.class); 170 | assertThat(response4.getStatusCode()).isEqualTo(HttpStatus.CREATED); 171 | } 172 | 173 | @Test 174 | @HttpStub( 175 | respond = { 176 | @Response(withStatus = HttpStatus.CREATED), 177 | @Response(withStatus = HttpStatus.OK), 178 | @Response(withStatus = HttpStatus.ACCEPTED) 179 | }) 180 | void testConsecutiveWithoutWrapAround() throws Exception { 181 | final ResponseEntity response1 = client().build().getForEntity(url("/"), String.class); 182 | assertThat(response1.getStatusCode()).isEqualTo(HttpStatus.CREATED); 183 | final ResponseEntity response2 = client().build().getForEntity(url("/"), String.class); 184 | assertThat(response2.getStatusCode()).isEqualTo(HttpStatus.OK); 185 | 186 | final ResponseEntity response3 = client().build().getForEntity(url("/"), String.class); 187 | assertThat(response3.getStatusCode()).isEqualTo(HttpStatus.ACCEPTED); 188 | 189 | assertThatExceptionOfType(HttpClientErrorException.class) 190 | .isThrownBy(() -> client().build().getForEntity(url("/"), String.class)) 191 | .satisfies(e -> { 192 | assertThat(e.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); 193 | }); 194 | } 195 | 196 | @Test 197 | @HttpStub( 198 | respond = { 199 | @Response(withStatus = HttpStatus.CREATED), 200 | @Response(withStatus = HttpStatus.OK), 201 | @Response(withStatus = HttpStatus.ACCEPTED) 202 | }, 203 | onLastResponse = WrapAround.REPEAT) 204 | void testConsecutiveWithWrapAroundRepeatLast() throws Exception { 205 | final ResponseEntity response1 = client().build().getForEntity(url("/"), String.class); 206 | assertThat(response1.getStatusCode()).isEqualTo(HttpStatus.CREATED); 207 | final ResponseEntity response2 = client().build().getForEntity(url("/"), String.class); 208 | assertThat(response2.getStatusCode()).isEqualTo(HttpStatus.OK); 209 | 210 | final ResponseEntity response3 = client().build().getForEntity(url("/"), String.class); 211 | assertThat(response3.getStatusCode()).isEqualTo(HttpStatus.ACCEPTED); 212 | 213 | final ResponseEntity response4 = client().build().getForEntity(url("/"), String.class); 214 | assertThat(response4.getStatusCode()).isEqualTo(HttpStatus.ACCEPTED); 215 | } 216 | 217 | @Test 218 | void testStubsInheritedFromInterface() throws Exception { 219 | final ResponseEntity response1 = client().build().getForEntity(url("/fromInterfaceCollection1"), 220 | String.class); 221 | assertThat(response1.getStatusCode()).isEqualTo(HttpStatus.OK); 222 | final ResponseEntity response2 = client().build().getForEntity(url("/fromInterfaceCollection2"), 223 | String.class); 224 | assertThat(response2.getStatusCode()).isEqualTo(HttpStatus.OK); 225 | } 226 | 227 | @Test 228 | void testStubsInheritedFromMetaAnnotation() throws Exception { 229 | final ResponseEntity response1 = client().build().getForEntity(url("/fromAnnotationCollection1"), 230 | String.class); 231 | assertThat(response1.getStatusCode()).isEqualTo(HttpStatus.OK); 232 | final ResponseEntity response2 = client().build().getForEntity(url("/fromAnnotationCollection2"), 233 | String.class); 234 | assertThat(response2.getStatusCode()).isEqualTo(HttpStatus.OK); 235 | } 236 | } 237 | --------------------------------------------------------------------------------