Spring Boot cookbook · Intermediate

Test external API integrations with WireMock

Test your RestClient-based integration with a payment API, including errors and timeouts, without calling the real service.

Approach

Start WireMock on a random port, point the client's base URL at it via @DynamicPropertySource, stub success, error and delay responses, and assert on how your code handles each. Verify requests (headers, body) where they matter.

Solution

@SpringBootTest
class RatesClientTest {
    @RegisterExtension
    static WireMockExtension wm = WireMockExtension.newInstance().options(wireMockConfig().dynamicPort()).build();

    @DynamicPropertySource
    static void props(DynamicPropertyRegistry r) {
        r.add("rates.base-url", wm::baseUrl);
    }

    @Autowired RatesService rates;

    @Test
    void returnsRate() {
        wm.stubFor(get(urlEqualTo("/rates/USD/EUR"))
                .willReturn(okJson("{\"rate\": 0.92}")));
        assertThat(rates.rate("USD", "EUR")).isEqualByComparingTo("0.92");
        wm.verify(getRequestedFor(urlEqualTo("/rates/USD/EUR")).withHeader("Authorization", matching("Bearer .+")));
    }

    @Test
    void mapsTimeoutToDomainError() {
        wm.stubFor(get(anyUrl()).willReturn(ok().withFixedDelay(6_000)));
        assertThatThrownBy(() -> rates.rate("USD", "EUR")).isInstanceOf(RatesUnavailable.class);
    }

    @Test
    void mapsUnknownCurrency() {
        wm.stubFor(get(anyUrl()).willReturn(aResponse().withStatus(404)));
        assertThatThrownBy(() -> rates.rate("USD", "XXX")).isInstanceOf(UnknownCurrency.class);
    }
}

Pitfalls

  • Only testing the happy path.
  • Hitting real third-party sandboxes in every CI run.
  • Timeout tests that take a long time (configure short timeouts in tests).

Variations

  • Spring Cloud Contract for consumer-driven contracts.
  • MockRestServiceServer for lightweight RestClient tests.

Further reading: WireMock: JUnit Jupiter

Reference: https://wiremock.org/docs/junit-jupiter/

More Spring Boot recipes