Testing REST API integrations using MockServer

  • Java
  • Spring Boot
  • REST API
  • MockServer
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 Mockserver 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 using Declarative HTTP Clients Support introduced in Spring Framework 6. Then we will test the external REST API integration using the Testcontainers Mockserver module.

Getting Started

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

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

testImplementation 'io.rest-assured:rest-assured'
testImplementation 'org.testcontainers:mockserver'
testImplementation 'org.mock-server:mockserver-netty:5.15.0'

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. MockServer is a library that can be used to mock or proxy any http or https based services. Testcontainers provides the Mockserver module so that we can run MockServer 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 interface

Prior to Spring Framework 6, either RestTemplate or WebClient or FeignClient was used to make HTTP API calls. Spring 6 introduced native support for creating Declarative HTTP Clients Support.

We will create an interface with a method to fetch photos for a given albumId as follows:

package com.testcontainers.demo;

import java.util.List;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.service.annotation.GetExchange;

interface PhotoServiceClient {
  @GetExchange("/albums/{albumId}/photos")
  List<Photo> getPhotos(@PathVariable Long albumId);
}

Register PhotoServiceClient as a Bean

In order to dynamically generate an implementation of PhotoServiceClient, we need to register it as a Spring bean using HttpServiceProxyFactory which needs an implementation of HttpClientAdapter. Spring Boot provides WebClientAdapter, which is an implementation of HttpClientAdapter and is part of spring-webflux library. As we have already added spring-boot-starter-webflux dependency, we can register PhotoServiceClient bean as follows:

package com.testcontainers.demo;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.support.WebClientAdapter;
import org.springframework.web.service.invoker.HttpServiceProxyFactory;

@Configuration
public class AppConfig {

  @Bean
  public PhotoServiceClient photoServiceClient(
    @Value("${photos.api.base-url}") String photosApiBaseUrl
  ) {
    WebClient client = WebClient.builder().baseUrl(photosApiBaseUrl).build();
    HttpServiceProxyFactory factory = HttpServiceProxyFactory
      .builder(WebClientAdapter.forClient(client))
      .build();
    return factory.createClient(PhotoServiceClient.class);
  }
}

We have externalized the photo-service base URL as a configurable property and is used to register the PhotoServiceClient bean. 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.reactive.function.client.WebClientResponseException;

@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 (WebClientResponseException 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 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 MockServer.

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 a MockServer using Testcontainers Mockserver module and create an instance of MockServerClient as follows:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
class AlbumControllerTest {
   @Container
   static MockServerContainer mockServerContainer =
        new MockServerContainer(DockerImageName.parse("mockserver/mockserver:5.15.0"));

   static MockServerClient mockServerClient;

   @DynamicPropertySource
   static void overrideProperties(DynamicPropertyRegistry registry) {
       mockServerClient = new MockServerClient(
           mockServerContainer.getHost(),
           mockServerContainer.getServerPort()
       );
       registry.add("photos.api.base-url", mockServerContainer::getEndpoint);
   }
   ...
   ...
}

Once we obtain a reference to mockServerClient, we can set the expectations for any API call as follows:

mockServerClient
   .when(request()
       .withMethod("GET")
       .withPath("/albums/1/photos"))
   .respond(response()
       .withStatusCode(200)
       .withBody("response content here"));

So, let us write the test for GET /api/albums/{albumId} endpoint as follows:

package com.testcontainers.demo;

import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.Matchers.hasSize;
import static org.mockserver.model.HttpRequest.request;
import static org.mockserver.model.HttpResponse.response;
import static org.mockserver.model.JsonBody.json;

import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockserver.client.MockServerClient;
import org.mockserver.model.Header;
import org.mockserver.verify.VerificationTimes;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.MockServerContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.utility.DockerImageName;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
class AlbumControllerTest {

  @LocalServerPort
  private Integer port;

  @Container
  static MockServerContainer mockServerContainer = new MockServerContainer(
    DockerImageName.parse("mockserver/mockserver:5.15.0")
  );

  static MockServerClient mockServerClient;

  @DynamicPropertySource
  static void overrideProperties(DynamicPropertyRegistry registry) {
    mockServerClient =
    new MockServerClient(
      mockServerContainer.getHost(),
      mockServerContainer.getServerPort()
    );
    registry.add("photos.api.base-url", mockServerContainer::getEndpoint);
  }

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

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

    mockServerClient
      .when(
        request().withMethod("GET").withPath("/albums/" + albumId + "/photos")
      )
      .respond(
        response()
          .withStatusCode(200)
          .withHeaders(
            new Header("Content-Type", "application/json; charset=utf-8")
          )
          .withBody(
            json(
              """
              [
                   {
                       "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));

    verifyMockServerRequest("GET", "/albums/" + albumId + "/photos", 1);
  }
  private void verifyMockServerRequest(String method, String path, int times) {
    mockServerClient.verify(
      request().withMethod(method).withPath(path),
      VerificationTimes.exactly(times)
    );
  }
}

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 baseURI.

  • We have used the Testcontainers JUnit 5 Extension annotations @Testcontainers and @Container to spin up a MockServerContainer and registered the photos.api.base-url property pointing to MockServer endpoint URL.

  • In @BeforeEach callback method, we are resetting the mockServerClient so that one test’s expectations won’t impact another test.

  • During 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.

  • Also, notice that we have verified whether the expected API call is made to MockServer or not using mockServerClient.verify().

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 MockServer 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 using Declarative HTTP Clients support and test it using Testcontainers Mockserver module.

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