Spring Boot + WireMock example testing REST Clients

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 the currency.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! 🚀