Introduction
Writing reliable tests for REST clients is crucial in modern application development. But testing external API calls can be tricky – we don’t want to depend on real services. WireMock helps us mock API responses efficiently. In this article, we’ll explore how to use WireMock in a Spring Boot project to test API calls with different scenarios.
What is WireMock?
WireMock is a powerful tool for mocking HTTP services. It allows us to simulate API responses for testing purposes, ensuring our application behaves correctly in different scenarios, even when the real API is unavailable.
Advantages of WireMock
- Isolated testing: No need to depend on external services.
- Customizable responses: Easily simulate various API behaviors.
- Error handling tests: Check how the application handles failures.
- Flexible integration: Works with different HTTP clients in Spring Boot.
Rest Clients in Spring Boot
The Spring Framework provides several clients for making REST calls:
- RestClient (Introduced in Spring 6.1) – Modern and flexible API client.
- WebClient – Reactive, non-blocking client for WebFlux.
- HTTP Interface – Declarative interface-based approach.
- RestTemplate – Traditional synchronous client (now considered legacy).
In this example, we will use RestClient to fetch data from a REST endpoint.
Project Setup
To get started, create a Spring Boot project using Spring Initializr with the following dependencies:
- Spring Web – to create REST APIs.
- Spring Boot Starter Test – for writing tests.
- WireMock – for mocking API responses.
pom.xml:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.4.3</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>tech.devblueprint</groupId> <artifactId>wiremock-spring-boot-testing-example</artifactId> <version>0.0.1-SNAPSHOT</version> <name>wiremock-spring-boot-testing-example</name> <description>Demonstration of wiremock tests</description> <url/> <properties> <java.version>17</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>com.maciejwalkowiak.spring</groupId> <artifactId>wiremock-spring-boot</artifactId> <version>2.1.2</version> <scope>test</scope> </dependency> </dependencies> <build> <finalName>project-name</finalName> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.1</version> <configuration> <source>17</source> <target>17</target> <annotationProcessorPaths> <path> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>${lombok.version}</version> </path> </annotationProcessorPaths> </configuration> </plugin> </plugins> </build> </project>
Final structure:
Now, let’s build the business logic step by step.
Writing Business Logic
1. Creating the Currency Client Interface
package tech.devblueprint.wiremock_spring_boot_testing_example.client; import tech.devblueprint.wiremock_spring_boot_testing_example.model.ExchangeRateResponse; public interface CurrencyClient { ExchangeRateResponse getExchangeRate(String base, String target); }
2. Implementing the Currency Client Service
package tech.devblueprint.wiremock_spring_boot_testing_example.client; import lombok.AllArgsConstructor; import org.springframework.http.HttpStatusCode; import org.springframework.http.MediaType; import org.springframework.stereotype.Component; import org.springframework.web.client.RestClient; import tech.devblueprint.wiremock_spring_boot_testing_example.model.ExchangeRateResponse; @Component @AllArgsConstructor public class CurrencyClientImpl implements CurrencyClient { private final RestClient currencyRestClient; @Override public ExchangeRateResponse getExchangeRate(String base, String target) { return currencyRestClient.get() .uri(uriBuilder -> uriBuilder .queryParam("base", base) .queryParam("symbols", target) .build()) .accept(MediaType.APPLICATION_JSON) .retrieve() .onStatus(HttpStatusCode::is4xxClientError, (req, res) -> { throw new RuntimeException("Client error while fetching exchange rate"); }) .onStatus(HttpStatusCode::is5xxServerError, (req, res) -> { throw new RuntimeException("Server error while fetching exchange rate"); }) .body(ExchangeRateResponse.class); } }
3. Creating the Exchange Rate Response Model
package tech.devblueprint.wiremock_spring_boot_testing_example.model; import lombok.Getter; import lombok.Setter; import java.math.BigDecimal; import java.util.Map; @Getter @Setter public class ExchangeRateResponse { private String base; private Map<String, BigDecimal> rates; }
4. Configuring the REST Client
package tech.devblueprint.wiremock_spring_boot_testing_example.сonfig; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.client.RestClient; @Configuration public class CurrencyApiClientConfig { @Value("${currency.api.url}") private String currencyApiUrl; @Bean public RestClient currencyRestClient() { return RestClient.create(currencyApiUrl); } }
At this point, we have a fully functional REST client that fetches exchange rates from an external API.
Writing Tests with WireMock
Now, let’s write tests for our REST client using WireMock.
1. Testing with
@EnableWireMock
- The
@EnableWireMock
annotation integrates WireMock with Spring Boot. - The
@ConfigureWireMock
annotation allows configuration through properties. - We mock API responses for testing without real API calls.
package tech.devblueprint.wiremock_spring_boot_testing_example.client; import com.maciejwalkowiak.wiremock.spring.ConfigureWireMock; import com.maciejwalkowiak.wiremock.spring.EnableWireMock; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import tech.devblueprint.wiremock_spring_boot_testing_example.model.ExchangeRateResponse; import static org.assertj.core.api.Assertions.assertThat; @SpringBootTest @EnableWireMock({ @ConfigureWireMock(name = "currency-client", property = "currency.api.url") }) public class CurrencyClientTestJsonBased { @Autowired private CurrencyClient currencyClient; @Test void shouldGetExchangeRate_whenFetchingValidCurrencyPair() { ExchangeRateResponse response = currencyClient.getExchangeRate("USD", "EUR"); assertThat(response.getBase()).isEqualTo("USD"); assertThat(response.getRates()).containsKey("EUR"); assertThat(response.getRates().get("EUR")).isEqualTo("0.85"); } }
Explanation of Annotations
@EnableWireMock
– Enables WireMock integration within Spring Boot and automatically starts a mock server.@ConfigureWireMock(name = "currency-client", property = "currency.api.url")
–
"currency-client"
: This can be any arbitrary string, it serves as the name of the WireMock instance.property = "currency.api.url"
: This tells WireMock to inject the mock server’s base URL into thecurrency.api.url
property. This means that the application will use the mocked API instead of a real API.
How does WireMock find the JSON stub file?
Since we set name = "currency-client"
, WireMock expects the stub file in:
📌 src/test/resources/wiremock/currency-client/mappings/
WireMock automatically loads stubs from the classpath directory:
📌 src/test/resources/wiremock/currency-client/mappings/get_usd_eur.json
{ "request": { "method": "GET", "urlPattern": "/\\?base=USD&symbols=EUR" }, "response": { "status": 200, "body": "{ \"base\": \"USD\", \"rates\": { \"EUR\": 0.85 } }", "headers": { "Content-Type": "application/json" } } }
2. Testing without
@EnableWireMock
- Here, we manually start and stop a WireMockServer.
- We stub API responses dynamically before each test.
Why are we NOT using @SpringBootTest
here?
Unlike the first test case, we are manually starting and stopping WireMock. Since we are handling the mock server entirely by ourselves, we don’t need to load the entire Spring Boot context. This makes the test lighter and faster.
package tech.devblueprint.wiremock_spring_boot_testing_example.client; import com.github.tomakehurst.wiremock.WireMockServer; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.web.client.RestTemplate; import tech.devblueprint.wiremock_spring_boot_testing_example.model.ExchangeRateResponse; import java.math.BigDecimal; import static com.github.tomakehurst.wiremock.client.WireMock.*; import static org.assertj.core.api.Assertions.assertThat; public class CurrencyClientTestWithoutEnableWireMock { private static WireMockServer wireMockServer; private final RestTemplate restTemplate = new RestTemplate(); private static String apiUrl; @BeforeAll static void startWireMock() { wireMockServer = new WireMockServer(8089); wireMockServer.start(); configureFor("localhost", 8089); apiUrl = "http://localhost:8089"; } @AfterAll static void stopWireMock() { if (wireMockServer != null) { wireMockServer.stop(); } } @BeforeEach void setupMocks() { wireMockServer.stubFor(get(urlEqualTo("/latest?base=USD&symbols=EUR")) .willReturn(aResponse() .withStatus(200) .withHeader("Content-Type", "application/json") .withBody("{ \"base\": \"USD\", \"rates\": { \"EUR\": 0.85 } }"))); } @Test void shouldGetExchangeRate_whenFetchingValidCurrencyPair() { String url = apiUrl + "/latest?base=USD&symbols=EUR"; ExchangeRateResponse response = restTemplate.getForObject(url, ExchangeRateResponse.class); assertThat(response).isNotNull(); assertThat(response.getBase()).isEqualTo("USD"); assertThat(response.getRates().get("EUR")).isEqualTo(BigDecimal.valueOf(0.85)); } }
3. Dynamic Stubbing with WireMock
- We dynamically modify API responses to test different scenarios.
- This helps verify how the application handles API changes.
package tech.devblueprint.wiremock_spring_boot_testing_example.client; import com.github.tomakehurst.wiremock.WireMockServer; import org.junit.jupiter.api.*; import static com.github.tomakehurst.wiremock.client.WireMock.*; import static org.assertj.core.api.Assertions.assertThat; import org.springframework.web.client.RestTemplate; public class DynamicWireMockTest { private static WireMockServer wireMockServer; private final RestTemplate restTemplate = new RestTemplate(); private static final String API_URL = "http://localhost:8089/latest?base=USD&symbols=EUR"; @BeforeAll static void startWireMock() { wireMockServer = new WireMockServer(8089); wireMockServer.start(); configureFor("localhost", 8089); } @AfterAll static void stopWireMock() { wireMockServer.stop(); } @Test void shouldReturnFirstMockResponse() { wireMockServer.removeStubMapping( wireMockServer.stubFor(get(urlEqualTo("/latest?base=USD&symbols=EUR")))); wireMockServer.stubFor(get(urlEqualTo("/latest?base=USD&symbols=EUR")) .willReturn(aResponse() .withStatus(200) .withHeader("Content-Type", "application/json") .withBody("{ \"base\": \"USD\", \"rates\": { \"EUR\": 0.85 } }"))); String response = restTemplate.getForObject(API_URL, String.class); System.out.println("First Response: " + response); assertThat(response).contains("\"EUR\": 0.85"); } @Test void shouldUpdateMockAndReturnNewResponse() { wireMockServer.removeStubMapping( wireMockServer.stubFor(get(urlEqualTo("/latest?base=USD&symbols=EUR")))); wireMockServer.stubFor(get(urlEqualTo("/latest?base=USD&symbols=EUR")) .willReturn(aResponse() .withStatus(200) .withHeader("Content-Type", "application/json") .withBody("{ \"base\": \"USD\", \"rates\": { \"EUR\": 0.90 } }"))); String response = restTemplate.getForObject(API_URL, String.class); System.out.println("Updated Response: " + response); assertThat(response).contains("\"EUR\": 0.90"); } }
Explaining Annotations
- Inside
@BeforeAll
– We Start WireMock before tests. - Inside
@AfterAll
– Stops WireMock after tests. - Inside
@BeforeEach
– Configures API responses before each test. stubFor()
– Defines the mock API behavior.removeStubMapping()
– Dynamically updates API responses.
Conclusion
In this article, we explored how to use WireMock for testing REST clients in Spring Boot. We covered: ✅ Setting up a Spring Boot project. ✅ Implementing a REST client using RestClient
. ✅ Writing unit tests with WireMock for isolated API testing. ✅ Handling dynamic API responses in tests.
By integrating WireMock, we can ensure our application’s resilience against API changes and failures. Now you’re ready to write effective tests for your REST clients! 🚀