WireMock with Spring Boot Example
1. Introduction
WireMock is a powerful HTTP mock server that stubs and verifies HTTP requests. It provides a controlled test environment, ensuring integration tests are fast, repeatable, and independent of external systems. In this example, I will demonstrate how to integrate WireMock into a Spring Boot project so the tests pass even when the third-party services become unavailable or return unexpected data.
2. Setup
In this step, I will create a gradle project with WireMock, spring-cloud-starter-contract-stub-runner, Lombok, and spring-boot-starter-web libraries via Spring Initializr.
2.1 Build.gradle
No modification for the generated build.gradle
file.
build.gradle
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 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 | plugins { id 'java' id 'org.springframework.boot' version '3.4.4' id 'io.spring.dependency-management' version '1.1.7' } group = 'com.zheng.demo' version = '0.0.1-SNAPSHOT' java { toolchain { languageVersion = JavaLanguageVersion.of(17) } } configurations { compileOnly { extendsFrom annotationProcessor } } repositories { mavenCentral() } ext { set('springCloudVersion', "2024.0.1") } dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.cloud:spring-cloud-starter-contract-stub-runner' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } dependencyManagement { imports { mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" } } tasks.named('test') { useJUnitPlatform() } |
2.2 Application.properties
Updated the generated application.properties
file to include the logging level and the third-party host URI.
application.properties
1 2 3 4 5 | spring.application.name=wiremock-demo 3rdParth.host=https://api.restful-api.dev/objects logging.level.org.zheng.demo=DEBUG |
- Line 3: The third-party API is a free API with a limit of 100 requests per day. Clients will receive
405 Method Not Allowed
when exceeding the limit.
2.3 logback.xml
Added the logback.xml
to enable the logging for the spring boot application.
logback.xml
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 | <? xml version = "1.0" encoding = "UTF-8" ?> < configuration > < include resource = "org/springframework/boot/logging/logback/defaults.xml" /> < include resource = "org/springframework/boot/logging/logback/console-appender.xml" /> < root level = "INFO" > < appender-ref ref = "CONSOLE" /> </ root > < logger name = "com.zheng.demo" level = "DEBUG" additivity = "false" > < appender-ref ref = "CONSOLE" /> </ logger > </ configuration > |
- Line 11: enable the log level to
DEBUG
.
2.4 Generated WiremockDemoApplication.java
Added a RestTempalte
bean in the generated WiremockDemoApplication.java
.
WiremockDemoApplication.java
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 | package com.zheng.demo.wiremock_demo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.web.client.RestTemplate; @SpringBootApplication public class WiremockDemoApplication { public static void main(String[] args) { SpringApplication.run(WiremockDemoApplication. class , args); } @Bean public RestTemplate restTemplate() { return new RestTemplate(); } } |
3. Java Model Object
3.1 ProductDetail.java
In this step, I will create a ProductDetail.java
class that matches the Rest API response.
ProductDetail.java
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | package com.zheng.demo.wiremock_demo.model; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor @AllArgsConstructor @Builder public class ProductDetail { private int year; private double price; @JsonProperty ( "CPU model" ) private String cpuModel; @JsonProperty ( "Hard disk size" ) private String hardDiskSize; } |
3.2 DemoObject.java
In this step, I will create a DemoObject.java
class that matches the Rest API response.
DemoObject.java
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | package com.zheng.demo.wiremock_demo.model; import java.time.LocalDateTime; import com.fasterxml.jackson.annotation.JsonInclude; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor @AllArgsConstructor @Builder @JsonInclude (JsonInclude.Include.NON_EMPTY) public class DemoObject { private String id; private String name; private ProductDetail data; private LocalDateTime updatedAt; } |
4. RestDemoController
In this step, I will create a RestDemoController.java
that invokes the third-party API for the CRUD operations.
RestDemoController.java
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 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 63 64 65 66 67 | package com.zheng.demo.wiremock_demo.rest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatusCode; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; import com.zheng.demo.wiremock_demo.model.DemoObject; @RestController @RequestMapping ( "/demo" ) public class RestDemoController { private static final String PATH = "/" ; private static final Logger logger = LoggerFactory.getLogger(RestDemoController. class ); @Value ( "${3rdParth.host}" ) private String thirdParthRestApiUrlBase; private final RestTemplate restTemplate; public RestDemoController(RestTemplate externalRestClient) { super (); this .restTemplate = externalRestClient; } @GetMapping ( "/{id}" ) public DemoObject getObjectById( @PathVariable ( "id" ) int id) { String url = thirdParthRestApiUrlBase + PATH + id; logger.info( "getObjectById url=" + url); return restTemplate.getForObject(url, DemoObject. class ); } @PostMapping public DemoObject createObject( @RequestBody DemoObject obj) { logger.info( "createObject url=" + thirdParthRestApiUrlBase); return restTemplate.postForObject(thirdParthRestApiUrlBase, obj, DemoObject. class ); } @PutMapping ( "/{id}" ) public ResponseEntity<DemoObject> updateObject( @PathVariable ( "id" ) int id, @RequestBody DemoObject obj) { String url = thirdParthRestApiUrlBase + PATH + id; logger.info( "updateObject url=" + url); restTemplate.put(url, obj, DemoObject. class ); return ResponseEntity.ok().body(getObjectById(id)); } @DeleteMapping ( "/{id}" ) public ResponseEntity<Object> deleteObjectById( @PathVariable ( "id" ) int id) { String url = thirdParthRestApiUrlBase + PATH + id; logger.info( "deleteObjectById url=" + url); restTemplate.delete(url); return ResponseEntity.status(HttpStatusCode.valueOf( 204 )).build(); } } |
- Line 28, 29: defines the
thirdParthRestApiUrlBase
varaible that gets the value from3rdParth.host
property in theapplication.properties
file. - Line 41, 47, 54, 62: prints out the logging for the third-party url. This will be used in 4.1 and 5.1.
4.1 Test the Spring Boot Application
In this step, I will start the spring boot application and test the Rest API via a curl command.
curl command for http://localhost:8080/demo/7
1 2 3 |
Here is the response.
http://localhost:8080/demo/7 Response
01 02 03 04 05 06 07 08 09 10 | { "id": "7", "name": "Apple MacBook Pro 16", "data": { "year": 2019, "price": 1849.99, "CPU model": "Intel Core i9", "Hard disk size": "1 TB" } } |
Monitor the spring boot application and capture the server log.
application log
1 | 2025-04-05T08:58:45.525-05:00 INFO 12028 --- [wiremock-demo] [nio-8080-exec-2] c.z.d.w.rest.RestDemoController : getObjectById url=https://api.restful-api.dev/objects/7 |
- Line 1: the application is hitting the actual third-party service –
https://api.restful-api.dev/objects/7
.
5. Spring boot Wiremock Test
In this step, I will create a RestDemoControllerTest.java
that stubs the CRUD options of the third-party API with a mock server.
RestDemoControllerTest.java
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 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 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | package com.zheng.demo.wiremock_demo.rest; import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; import static com.github.tomakehurst.wiremock.client.WireMock.delete; import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; import static com.github.tomakehurst.wiremock.client.WireMock.get; import static com.github.tomakehurst.wiremock.client.WireMock.post; import static com.github.tomakehurst.wiremock.client.WireMock.put; import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.cloud.contract.wiremock.AutoConfigureWireMock; import org.springframework.http.ResponseEntity; import org.springframework.test.util.ReflectionTestUtils; import com.zheng.demo.wiremock_demo.model.DemoObject; import com.zheng.demo.wiremock_demo.model.ProductDetail; @SpringBootTest @AutoConfigureWireMock (port = 8090 ) class RestDemoControllerTest { private static final String OBJECT_JSON = "{\"id\":\"7\",\"name\":\"Apple MacBook Pro 16\",\"data\":{\"year\":2019,\"price\":1849.99,\"CPU model\":\"Intel Core i9\",\"Hard disk size\":\"1 TB\"}}" ; @Autowired private RestDemoController testClass; @BeforeEach void setup() { } @Test void test_getObjectById() { stubFor(get(urlEqualTo( "/7" )).willReturn( aResponse().withStatus( 200 ).withHeader( "Content-Type" , "application/json" ).withBody(OBJECT_JSON))); DemoObject rep = testClass.getObjectById( 7 ); assertEquals( "7" , rep.getId()); } @Test void test_createObject() { stubFor(post(urlEqualTo( "/" )).withRequestBody(equalToJson(OBJECT_JSON)).willReturn( aResponse().withStatus( 200 ).withHeader( "Content-Type" , "application/json" ).withBody(OBJECT_JSON))); ProductDetail pd = ProductDetail.builder().year( 2019 ).price( 1849.99 ).cpuModel( "Intel Core i9" ) .hardDiskSize( "1 TB" ).build(); DemoObject obj = DemoObject.builder().id( "7" ).name( "Apple MacBook Pro 16" ).data(pd).build(); DemoObject rep = testClass.createObject(obj); assertEquals( "7" , rep.getId()); } @Test void test_putObject() { stubFor(put(urlEqualTo( "/7" )).withRequestBody(equalToJson(OBJECT_JSON)) .willReturn(aResponse().withStatus( 204 ))); stubFor(get(urlEqualTo( "/7" )).willReturn( aResponse().withStatus( 200 ).withHeader( "Content-Type" , "application/json" ).withBody(OBJECT_JSON))); ProductDetail pd = ProductDetail.builder().year( 2019 ).price( 1849.99 ).cpuModel( "Intel Core i9" ) .hardDiskSize( "1 TB" ).build(); DemoObject obj = DemoObject.builder().id( "7" ).name( "Apple MacBook Pro 16" ).data(pd).build(); ResponseEntity<DemoObject> rep = testClass.updateObject( 7 , obj); assertEquals( "7" , rep.getBody().getId()); } @Test void test_deleteObject() { stubFor(delete(urlEqualTo( "/7" )).willReturn(aResponse().withStatus( 204 ))); ResponseEntity<Object> rep = testClass.deleteObjectById( 7 ); assertTrue(rep.getStatusCode().is2xxSuccessful()); } } |
- Line 26:
@AutoConfigureWireMock(port = 8090)
starts the mock server at port8090
. - Line 35: use the ReflectionTestUtils‘s
setField
method to set thetestClass
.thirdParthRestApiUrlBase
to the mock server:http://localhost:8090
. - Line 40, 49, 60, 62, 73: use WireMock to stub Get, Post, Put, and Delete operations’ requests and responses.
5.1 Execute Spring boot Wiremock Test
In this step, I will execute the RestDemoControllerTest.java
and capture the console log.
Mock Testing Console Log
001 002 003 004 005 006 007 008 009 010 011 012 013 014 015 016 017 018 019 020 021 022 023 024 025 026 027 028 029 030 031 032 033 034 035 036 037 038 039 040 041 042 043 044 045 046 047 048 049 050 051 052 053 054 055 056 057 058 059 060 061 062 063 064 065 066 067 068 069 070 071 072 073 074 075 076 077 078 079 080 081 082 083 084 085 086 087 088 089 090 091 092 093 094 095 096 097 098 099 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 | 2025-04-05T10:16:26.920-05:00 INFO --- [ main] t.c.s.AnnotationConfigContextLoaderUtils : Could not detect default configuration classes for test class [com.zheng.demo.wiremock_demo.rest.RestDemoControllerTest]: RestDemoControllerTest does not declare any static, non-private, non-final, nested classes annotated with @Configuration. 2025-04-05T10:16:27.090-05:00 INFO --- [ main] .b.t.c.SpringBootTestContextBootstrapper : Found @SpringBootConfiguration com.zheng.demo.wiremock_demo.WiremockDemoApplication for test class com.zheng.demo.wiremock_demo.rest.RestDemoControllerTest . ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v3.4.4) 2025-04-05T10:16:27.514-05:00 INFO 27828 --- [wiremock-demo] [ main] c.z.d.w.rest.RestDemoControllerTest : Starting RestDemoControllerTest using Java 17.0.11 with PID 27828 (started by azpm0 in C:\MaryTools\workspace\wiremock-demo) 2025-04-05T10:16:27.514-05:00 DEBUG 27828 --- [wiremock-demo] [ main] c.z.d.w.rest.RestDemoControllerTest : Running with Spring Boot v3.4.4, Spring v6.2.5 2025-04-05T10:16:27.515-05:00 INFO 27828 --- [wiremock-demo] [ main] c.z.d.w.rest.RestDemoControllerTest : No active profile set, falling back to 1 default profile: "default" 2025-04-05T10:16:29.710-05:00 INFO 27828 --- [wiremock-demo] [ main] c.z.d.w.rest.RestDemoControllerTest : Started RestDemoControllerTest in 2.467 seconds (process running for 3.726) 2025-04-05T10:16:30.592-05:00 INFO 27828 --- [wiremock-demo] [ main] c.z.d.w.rest.RestDemoController : deleteObjectById url=http://localhost:8090/7 2025-04-05T10:16:30.758-05:00 INFO 27828 --- [wiremock-demo] [tp2061865206-32] WireMock : Request received: 127.0.0.1 - DELETE /7 Accept: [*/*] User-Agent: [Java/17.0.11] Host: [localhost:8090] Connection: [keep-alive] Content-Type: [application/x-www-form-urlencoded] Transfer-Encoding: [chunked] Matched response definition: { "status" : 204 } Response: HTTP/1.1 204 Matched-Stub-Id: [45e076f0-5261-4a0b-9dbb-97b2e8e9b793] 2025-04-05T10:16:30.797-05:00 INFO 27828 --- [wiremock-demo] [ main] c.z.d.w.rest.RestDemoController : updateObject url=http://localhost:8090/7 2025-04-05T10:16:30.862-05:00 INFO 27828 --- [wiremock-demo] [tp2061865206-30] WireMock : Request received: 127.0.0.1 - PUT /7 Content-Type: [application/json] Accept: [*/*] User-Agent: [Java/17.0.11] Host: [localhost:8090] Connection: [keep-alive] Transfer-Encoding: [chunked] {"id":"7","name":"Apple MacBook Pro 16","data":{"year":2019,"price":1849.99,"CPU model":"Intel Core i9","Hard disk size":"1 TB"}} Matched response definition: { "status" : 204 } Response: HTTP/1.1 204 Matched-Stub-Id: [38f980f5-973c-4607-8af4-88fdc4e6750b] 2025-04-05T10:16:30.863-05:00 INFO 27828 --- [wiremock-demo] [ main] c.z.d.w.rest.RestDemoController : getObjectById url=http://localhost:8090/7 2025-04-05T10:16:30.895-05:00 INFO 27828 --- [wiremock-demo] [tp2061865206-32] WireMock : Request received: 127.0.0.1 - GET /7 Accept: [application/json, application/yaml, application/*+json] User-Agent: [Java/17.0.11] Host: [localhost:8090] Connection: [keep-alive] Matched response definition: { "status" : 200, "body" : "{\"id\":\"7\",\"name\":\"Apple MacBook Pro 16\",\"data\":{\"year\":2019,\"price\":1849.99,\"CPU model\":\"Intel Core i9\",\"Hard disk size\":\"1 TB\"}}", "headers" : { "Content-Type" : "application/json" } } Response: HTTP/1.1 200 Content-Type: [application/json] Matched-Stub-Id: [d60084b6-7f40-4f79-98e0-d25a4d2ee2ed] 2025-04-05T10:16:30.911-05:00 INFO 27828 --- [wiremock-demo] [ main] c.z.d.w.rest.RestDemoController : getObjectById url=http://localhost:8090/7 2025-04-05T10:16:30.913-05:00 INFO 27828 --- [wiremock-demo] [tp2061865206-30] WireMock : Request received: 127.0.0.1 - GET /7 Accept: [application/json, application/yaml, application/*+json] User-Agent: [Java/17.0.11] Host: [localhost:8090] Connection: [keep-alive] Matched response definition: { "status" : 200, "body" : "{\"id\":\"7\",\"name\":\"Apple MacBook Pro 16\",\"data\":{\"year\":2019,\"price\":1849.99,\"CPU model\":\"Intel Core i9\",\"Hard disk size\":\"1 TB\"}}", "headers" : { "Content-Type" : "application/json" } } Response: HTTP/1.1 200 Content-Type: [application/json] Matched-Stub-Id: [0930abe7-4bec-4762-90f8-9989ec165b4d] 2025-04-05T10:16:30.920-05:00 INFO 27828 --- [wiremock-demo] [ main] c.z.d.w.rest.RestDemoController : createObject url=http://localhost:8090 2025-04-05T10:16:30.934-05:00 INFO 27828 --- [wiremock-demo] [tp2061865206-32] WireMock : Request received: 127.0.0.1 - POST / Accept: [application/json, application/yaml, application/*+json] Content-Type: [application/json] User-Agent: [Java/17.0.11] Host: [localhost:8090] Connection: [keep-alive] Transfer-Encoding: [chunked] {"id":"7","name":"Apple MacBook Pro 16","data":{"year":2019,"price":1849.99,"CPU model":"Intel Core i9","Hard disk size":"1 TB"}} Matched response definition: { "status" : 200, "body" : "{\"id\":\"7\",\"name\":\"Apple MacBook Pro 16\",\"data\":{\"year\":2019,\"price\":1849.99,\"CPU model\":\"Intel Core i9\",\"Hard disk size\":\"1 TB\"}}", "headers" : { "Content-Type" : "application/json" } } Response: HTTP/1.1 200 Content-Type: [application/json] Matched-Stub-Id: [97a6cc5b-8a01-4ad4-8301-6c56bf32b790] 2025-04-05T10:16:30.938-05:00 WARN 27828 --- [wiremock-demo] [ main] o.s.c.c.w.WireMockTestExecutionListener : You've used fixed ports for WireMock setup - will mark context as dirty. Please use random ports, as much as possible. Your tests will be faster and more reliable and this warning will go away |
- Highlighted lines print the mock server URL:
localhost:8090
instead of the actual services.
6. Conclusion
Testing external dependencies such as REST APIs can be challenging when developing web applications. Making network calls is slow and unreliable. WireMock provides a controlled test environment, ensuring integration tests are fast, repeatable, and independent of external systems. In this example, I created a spring boot web application that utilized Wiremock to stub the third-party API requests and responses.
7. Download
This was an example of a gradle project that included wiremock in a spring boot application.
You can download the full source code of this example here: WireMock with Spring Boot Example