Testing the Integration Layer of Your Spring Boot Application with Testcontainers and MockServer

In modern microservices architectures, robust and reliable integration testing is crucial to ensure seamless communication between services. Integration tests verify that different parts of the application work together as expected. This blog post will dive into how to effectively test the integration layer of your Spring Boot application using Testcontainers and MockServer, two powerful tools that simplify and enhance the testing process.

What is Integration Testing?

Integration testing is a level of software testing where individual units or components are combined and tested as a group. The primary goal is to identify issues related to the interaction between integrated components. Unlike unit tests, which isolate and test individual pieces of code, integration tests verify the correct functioning of interconnected components, such as databases, external APIs, and other services.

Why Mock Responses Instead of Real APIs?

Mocking responses during integration testing offers several benefits:

  1. Isolation: Testing with real APIs can introduce variability and dependencies that may cause tests to fail due to issues beyond your control. Mocking isolates your tests from external services, ensuring consistent and predictable test results.
  2. Speed: Mocked responses are typically faster than real API calls, leading to quicker test execution and a more efficient development workflow.
  3. Availability: Real APIs may not always be available or may have rate limits, making them unreliable for automated testing environments. MockServer provides a stable and controllable alternative.

What is Testcontainers?

Testcontainers is a Java library that supports JUnit tests, providing lightweight, disposable instances of common databases, Selenium web browsers, or anything else that can run in a Docker container. It ensures a consistent, clean environment for integration tests, eliminating the “works on my machine” problem.

Key Benefits of Testcontainers

  • Isolation: Each test runs in its own container, ensuring no side effects between tests.
  • Consistency: Provides a consistent environment regardless of the underlying OS.
  • Reproducibility: Ensures the same environment for both local development and CI/CD pipelines.

Setting Up Testcontainers and MockServer

Dependencies

First, include the necessary dependencies in your pom.xml:

<dependencies>
    <!-- Spring Boot dependencies -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <!-- Testcontainers dependencies -->
    <dependency>
        <groupId>org.testcontainers</groupId>
        <artifactId>testcontainers</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.testcontainers</groupId>
        <artifactId>junit-jupiter</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.testcontainers</groupId>
        <artifactId>mockserver</artifactId>
        <scope>test</scope>
    </dependency>
    <!-- Feign dependencies -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
</dependencies>

Configuring MockServer with Testcontainers

Next, configure MockServer to run within a Testcontainer in your integration test class:

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockserver.client.MockServerClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.testcontainers.containers.MockServerContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

import static org.mockserver.model.HttpRequest.request;
import static org.mockserver.model.HttpResponse.response;

@ExtendWith(SpringExtension.class)
@SpringBootTest
@Testcontainers
@EnableFeignClients
public class IntegrationTest {

    @Container
    public MockServerContainer mockServer = new MockServerContainer();

    private MockServerClient mockServerClient;

    @Autowired
    private SomeService someService; // Your service that makes the API call

    @BeforeEach
    public void setUp() {
        mockServerClient = new MockServerClient(mockServer.getHost(), mockServer.getServerPort());
    }

    @Test
    public void testServiceWithMockedResponse() {
        // Set up MockServer to return a mock response
        mockServerClient.when(
            request()
                .withMethod("GET")
                .withPath("/api/some-endpoint")
        ).respond(
            response()
                .withStatusCode(200)
                .withBody("{ \\"key\\": \\"value\\" }")
        );

        // Perform the service call
        String result = someService.callExternalApi();

        // Verify the result
        assertEquals("value", result);
    }
}

Feign Client Interface

Create a Feign client interface to define the API endpoint:

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;

@FeignClient(name = "exampleClient", url = "${mockserver.url}")
public interface ExampleClient {
    @GetMapping("/api/some-endpoint")
    ApiResponse getSomeData();
}

class ApiResponse {
    private String key;

    // getters and setters

    public String getKey() {
        return key;
    }

    public void setKey(String key) {
        this.key = key;
    }
}

Service Implementation Example

Here’s a simple service that uses the Feign client to make an external API call:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class SomeService {

    @Autowired
    private ExampleClient exampleClient;

    public String callExternalApi() {
        ApiResponse response = exampleClient.getSomeData();
        return response.getKey();
    }
}

Configuration for MockServer URL

To dynamically set the MockServer URL, add a @TestPropertySource to your test class:

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockserver.client.MockServerClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.testcontainers.containers.MockServerContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

import static org.mockserver.model.HttpRequest.request;
import static org.mockserver.model.HttpResponse.response;

@ExtendWith(SpringExtension.class)
@SpringBootTest
@Testcontainers
@EnableFeignClients
@TestPropertySource(properties = {
    "mockserver.url=http://${mockserver.host}:${mockserver.port}"
})
public class IntegrationTest {

    @Container
    public MockServerContainer mockServer = new MockServerContainer();

    private MockServerClient mockServerClient;

    @Autowired
    private SomeService someService; // Your service that makes the API call

    @BeforeEach
    public void setUp() {
        mockServerClient = new MockServerClient(mockServer.getHost(), mockServer.getServerPort());
        System.setProperty("mockserver.host", mockServer.getHost());
        System.setProperty("mockserver.port", String.valueOf(mockServer.getServerPort()));
    }

    @Test
    public void testServiceWithMockedResponse() {
        // Set up MockServer to return a mock response
        mockServerClient.when(
            request()
                .withMethod("GET")
                .withPath("/api/some-endpoint")
        ).respond(
            response()
                .withStatusCode(200)
                .withBody("{ \\"key\\": \\"value\\" }")
        );

        // Perform the service call
        String result = someService.callExternalApi();

        // Verify the result
        assertEquals("value", result);
    }
}

When you run your test, Testcontainers will automatically start the MockServer container, configure the mock response, and then execute your test. The mocked response ensures that your test is isolated, fast, and reliable.

Conclusion

Testing the integration layer of your Spring Boot application with Testcontainers and MockServer provides a robust and reliable approach to ensure your services interact correctly. By mocking external API responses, you gain control over your test environment, leading to more predictable and maintainable tests. Testcontainers further enhances this setup by providing isolated and reproducible environments for your tests, ensuring consistency across different stages of development and deployment.

Implementing these practices will significantly improve the quality and reliability of your integration tests, making your overall development process more efficient and resilient. Happy testing!