🚨 AtomicJar is now part of Docker πŸ‹! Read the blog

Testing REST API integrations using WireMock

  • Java
  • Spring Boot
  • REST API
  • WireMock
Get the code

In this guide, you will learn how to

  • Create a Spring Boot application which talks to external REST APIs

  • Test the external API integration using Testcontainers WireMock module

Prerequisites

What we are going to achieve in this guide

We are going to create a Spring Boot project which talks to an external REST API. Then we will test the external REST API integration using the Testcontainers WireMock module.

Getting Started

You can create a new Spring Boot project from Spring Initializr by selecting the Spring Web, and Testcontainers starters.

Once the application is generated, add the RestAssured and WireMock libraries as test dependencies.

repositories {
    mavenCentral()
}

ext {
    set('wiremockTestcontainersVersion', "1.0-alpha-13")
}

dependencies {
    testImplementation 'io.rest-assured:rest-assured'
    testImplementation 'org.wiremock:wiremock-standalone:3.6.0'
    testImplementation "org.wiremock.integrations.testcontainers:wiremock-testcontainers-module:${wiremockTestcontainersVersion}"
}

Assume we are building an application to manage video albums, and we are going to use a 3rd party REST API to manage the image and video assets. For this guide, we are going to use a publicly available REST API https://jsonplaceholder.typicode.com/ as a 3rd party photo-service to store album photos.

We will implement a REST API endpoint to fetch an album for the given albumId. This API internally talks to the photo-service to fetch the photos for that album.

We will use WireMock, which is a tool for building mock APIs, to mock the external service interactions and test our API endpoints. Testcontainers provides the Testcontainers WireMock module so that we can run WireMock as a Docker container.

Create Album and Photo models

First, let us create Album and Photo models using Java records.

package com.testcontainers.demo;

import java.util.List;

public record Album(Long albumId, List<Photo> photos) {}

record Photo(Long id, String title, String url, String thumbnailUrl) {}

Create PhotoServiceClient

Let’s create PhotoServiceClient, which internally uses RestTemplate, to fetch photos for a given albumId.

package com.testcontainers.demo;

import java.util.List;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

@Service
class PhotoServiceClient {

  private final String baseUrl;
  private final RestTemplate restTemplate;

  PhotoServiceClient(
    @Value("${photos.api.base-url}") String baseUrl,
    RestTemplateBuilder builder
  ) {
    this.baseUrl = baseUrl;
    this.restTemplate = builder.build();
  }

  List<Photo> getPhotos(Long albumId) {
    String url = baseUrl + "/albums/{albumId}/photos";
    ResponseEntity<List<Photo>> response = restTemplate.exchange(
      url,
      HttpMethod.GET,
      null,
      new ParameterizedTypeReference<>() {},
      albumId
    );
    return response.getBody();
  }
}

We have externalized the photo-service base URL as a configurable property. So, let us add the following property in the src/main/resources/application.properties file.

photos.api.base-url=https://jsonplaceholder.typicode.com

Implement API endpoint to get album by id

Let us implement a REST API endpoint to return an Album for the given albumId as follows:

package com.testcontainers.demo;

import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestClientResponseException;

@RestController
@RequestMapping("/api")
class AlbumController {

  private static final Logger logger = LoggerFactory.getLogger(
    AlbumController.class
  );

  private final PhotoServiceClient photoServiceClient;

  AlbumController(PhotoServiceClient photoServiceClient) {
    this.photoServiceClient = photoServiceClient;
  }

  @GetMapping("/albums/{albumId}")
  public ResponseEntity<Album> getAlbumById(@PathVariable Long albumId) {
    try {
      List<Photo> photos = photoServiceClient.getPhotos(albumId);
      return ResponseEntity.ok(new Album(albumId, photos));
    } catch (RestClientResponseException e) {
      logger.error("Failed to get photos", e);
      return new ResponseEntity<>(e.getStatusCode());
    }
  }
}

Our application is exposing a REST API endpoint GET /api/albums/{albumId} which internally makes an API call to https://jsonplaceholder.typicode.com/albums/{albumId}/photos to get photos of that album and returns a response similar to the following:

{
   "albumId": 1,
   "photos": [
       {
           "id": 51,
           "title": "non sunt voluptatem placeat consequuntur rem incidunt",
           "url": "https://via.placeholder.com/600/8e973b",
           "thumbnailUrl": "https://via.placeholder.com/150/8e973b"
       },
       {
           "id": 52,
           "title": "eveniet pariatur quia nobis reiciendis laboriosam ea",
           "url": "https://via.placeholder.com/600/121fa4",
           "thumbnailUrl": "https://via.placeholder.com/150/121fa4"
       },
       ...
       ...
   ]
}

You can run the application and access http://localhost:8080/api/albums/1 to see the JSON response.

Now, let us see how we can test the photo-service API integration using WireMock.

Write test for photo-service API integration

It is better to mock the external API interactions at HTTP protocol level instead of mocking the photoServiceClient.getPhotos(albumId) method because you will be able to verify any marshaling/unmarshalling errors, simulate network latency issues, etc.

We can create an instance of WireMock server using WireMockExtension and set the expectations for any API call as follows:

@RegisterExtension
static WireMockExtension wireMock = WireMockExtension.newInstance()
        .options(wireMockConfig().dynamicPort())
        .build();

wireMock.stubFor(
   WireMock.get(urlMatching("/albums/*/photos"))
    .willReturn(
      aResponse()
        .withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
        .withBody(
        """
        [
           {
               "id": 1,
               "title": "accusamus beatae ad facilis cum similique qui sunt",
               "url": "https://via.placeholder.com/600/92c952",
               "thumbnailUrl": "https://via.placeholder.com/150/92c952"
           }
        ]
        """))
);

Let us write the test for our GET /api/albums/{albumId} API endpoint as follows:

package com.testcontainers.demo;

import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
import static com.github.tomakehurst.wiremock.client.WireMock.urlMatching;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.Matchers.hasSize;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;

import com.github.tomakehurst.wiremock.client.WireMock;
import com.github.tomakehurst.wiremock.junit5.WireMockExtension;
import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.http.MediaType;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;

@SpringBootTest(webEnvironment = RANDOM_PORT)
class AlbumControllerTest {

  @LocalServerPort
  private Integer port;

  @RegisterExtension
  static WireMockExtension wireMock = WireMockExtension
    .newInstance()
    .options(wireMockConfig().dynamicPort())
    .build();

  @DynamicPropertySource
  static void configureProperties(DynamicPropertyRegistry registry) {
    registry.add("photos.api.base-url", wireMock::baseUrl);
  }

  @BeforeEach
  void setUp() {
    RestAssured.port = port;
  }

  @Test
  void shouldGetAlbumById() {
    Long albumId = 1L;

    wireMock.stubFor(
      WireMock
        .get(urlMatching("/albums/" + albumId + "/photos"))
        .willReturn(
          aResponse()
            .withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
            .withBody(
              """
              [
                   {
                       "id": 1,
                       "title": "accusamus beatae ad facilis cum similique qui sunt",
                       "url": "https://via.placeholder.com/600/92c952",
                       "thumbnailUrl": "https://via.placeholder.com/150/92c952"
                   },
                   {
                       "id": 2,
                       "title": "reprehenderit est deserunt velit ipsam",
                       "url": "https://via.placeholder.com/600/771796",
                       "thumbnailUrl": "https://via.placeholder.com/150/771796"
                   }
               ]
              """
            )
        )
    );

    given()
      .contentType(ContentType.JSON)
      .when()
      .get("/api/albums/{albumId}", albumId)
      .then()
      .statusCode(200)
      .body("albumId", is(albumId.intValue()))
      .body("photos", hasSize(2));
  }

  @Test
  void shouldReturnServerErrorWhenPhotoServiceCallFailed() {
    Long albumId = 2L;
    wireMock.stubFor(
      WireMock
        .get(urlMatching("/albums/" + albumId + "/photos"))
        .willReturn(aResponse().withStatus(500))
    );

    given()
      .contentType(ContentType.JSON)
      .when()
      .get("/api/albums/{albumId}", albumId)
      .then()
      .statusCode(500);
  }
}

Let us understand what is going on in this test.

  • We have annotated the test class with the @SpringBootTest annotation to write an integration test which loads the complete Spring application context.

  • We are using the RestAssured library to test our API endpoint, so we captured the random port on which the application started and initialized RestAssured port.

  • We have registered the photos.api.base-url property pointing to WireMock endpoint URL.

  • In the shouldGetAlbumById() test, we have set the expected mock response for /albums/{albumId}/photos API call and make a request to our application endpoint /api/albums/{albumId} and verified the response.

  • In the shouldReturnServerErrorWhenPhotoServiceCallFailed() test, we have set the expected mock response for /albums/{albumId}/photos API call to return InternalServerError status code 500 and make a request to our application endpoint /api/albums/{albumId} and verified the response.

Stubbing using JSON mapping files

In the previous test, we saw how to stub an API using wireMock.stubFor(…​). Instead of stubbing using WireMock Java API, we can use JSON mapping based configuration.

Create src/test/resources/wiremock/mappings/get-album-photos.json file as follows:

{
  "mappings": [
    {
      "request": {
        "method": "GET",
        "urlPattern": "/albums/([0-9]+)/photos"
      },
      "response": {
        "status": 200,
        "headers": {
          "Content-Type": "application/json"
        },
        "jsonBody": [
          {
            "id": 1,
            "title": "accusamus beatae ad facilis cum similique qui sunt",
            "url": "https://via.placeholder.com/600/92c952",
            "thumbnailUrl": "https://via.placeholder.com/150/92c952"
          },
          {
            ...
          }
        ]
      }
    }
  ]
}

Now you can initialize WireMock by loading the stub mappings from mapping files as follows:

@RegisterExtension
static WireMockExtension wireMock = WireMockExtension.newInstance()
     .options(
         wireMockConfig()
            .dynamicPort()
            .usingFilesUnderClasspath("wiremock")
    )
    .build();

With mapping files based stubbing in place, you can write tests as follows:

  @Test
  void shouldGetAlbumById() {
    Long albumId = 1L;

    given()
      .contentType(ContentType.JSON)
      .when()
      .get("/api/albums/{albumId}", albumId)
      .then()
      .statusCode(200)
      .body("albumId", is(albumId.intValue()))
      .body("photos", hasSize(2));
  }

Using Testcontainers WireMock Module

The Testcontainers WireMock module allows provisioning the WireMock server as a standalone container within your tests, based on WireMock Docker.

Create AlbumControllerTestcontainersTests and use WireMockContainer to initialize a wiremock server and stubbing as follows:

@SpringBootTest(webEnvironment = RANDOM_PORT)
@Testcontainers
class AlbumControllerTestcontainersTests {

    @LocalServerPort
    private Integer port;

    @Container
    static WireMockContainer wiremockServer = new WireMockContainer("wiremock/wiremock:3.6.0")
            .withMapping("photos-by-album",
                        AlbumControllerTestcontainersTests.class,
                        "mocks-config.json");

    @DynamicPropertySource
    static void configureProperties(DynamicPropertyRegistry registry) {
        registry.add("photos.api.base-url", wiremockServer::getBaseUrl);
    }

    @BeforeEach
    void setUp() {
        RestAssured.port = port;
    }

    @Test
    void shouldGetAlbumById() {
        Long albumId = 1L;

        given().contentType(ContentType.JSON)
                .when()
                .get("/api/albums/{albumId}", albumId)
                .then()
                .statusCode(200)
                .body("albumId", is(albumId.intValue()))
                .body("photos", hasSize(1));
    }
}
  • We are using Testcontainers JUnit 5 Extension annotations @Testcontainers and @Container to initialize WireMockContainer.

  • We have configured to load stub mappings from mocks-config.json file

Create src/test/resources/com/testcontainers/demo/AlbumControllerTestcontainersTests/mocks-config.json file as follows:

{
  "mappings": [
    {
      "request": {
        "method": "GET",
        "urlPattern": "/albums/([0-9]+)/photos"
      },
      "response": {
        "status": 200,
        "headers": {
          "Content-Type": "application/json"
        },
        "jsonBody": [
          {
            "id": 1,
            "title": "accusamus beatae ad facilis cum similique qui sunt",
            "url": "https://via.placeholder.com/600/92c952",
            "thumbnailUrl": "https://via.placeholder.com/150/92c952"
          }
        ]
      }
    }
  ]
}

Now if you run the test, the call to photo-service API will receive the response using WireMock stubbings defined in mocks-config.json file.

Run tests

# If you are using Maven
./mvnw test

# If you are using Gradle
./gradlew test

Now, if you run your test, you should see in the console log that WireMock Docker instance is started which will act as the photo-service, serving the mock responses as per the configured expectations and the test should pass.

Summary

We have learned how to integrate 3rd party HTTP APIs in a Spring Boot application and test it using Testcontainers WireMock module.

Tip

Testcontainers WireMock modules are available for Go and Python languages as well.

For more information on using Testcontainers WireMock module, please refer the documentation at https://wiremock.org/docs/solutions/testcontainers/.

To learn more about Testcontainers visit http://testcontainers.com