DEV Community

Thomas
Thomas

Posted on • Updated on • Originally published at bootify.io

Adding Integration Tests in Spring Boot with Testcontainers

Good software always includes automated tests to detect errors during the creation and modification of the code. In this tutorial, integration tests will be added to a Spring Boot application, connecting to the 'real' database using the Testcontainers library.

Backgrounds

An integration test is meant to test the interaction of multiple parts of the application. So for a Spring Boot application, it would be great to examine the running app including the database (e.g. MySQL, MariaDB or PostgreSQL). An embedded database would not be optimal for this, as there can be significant differences to the actual database, and therefore certain features may not work or errors may be missed.

For this purpose, there is the Testcontainers library to load a Docker image of exactly the database that is used by the developer and later in production. To write an integration test for our application, we first add the dependencies.

testImplementation('org.testcontainers:junit-jupiter:1.15.0')
testImplementation('org.testcontainers:mysql:1.15.0')
testImplementation('org.springframework.boot:spring-boot-starter-test')
Enter fullscreen mode Exit fullscreen mode

  Dependencies of build.gradle

Besides spring-boot-starter-test we need org.testcontainers:junit-jupiter as well as an org.testcontainers reference to the database we have in use. In our case we choose MySQL.

Starting the application context

To centralize the recurring logic of our test classes, we first create an abstract base class from which our IT classes then inherit. The first version of this class looks like this:

@SpringBootTest(classes = MyAppApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("it")
public abstract class BaseIT {

    @Autowired
    public TestRestTemplate restTemplate;

}
Enter fullscreen mode Exit fullscreen mode

  First version of BaseIT.java

Spring Boot provides very good support for integration testing by using the @SpringBootTest annotation to load the full application context. With @ActiveProfiles we define that the application is started with the profile "it". If needed, we can use this profile to activate special settings in our code.

By setting the variable webEnvironment to RANDOM_PORT the application will be started under a random (and available) port. This will be picked up automatically by the TestRestTemplate, when using it to send calls to our application's REST API - more on that later.

Mocking the database

Now we want to configure a database that is exclusively available for our tests. For this we add a file application-it.yml / application-it.properties in our src/test/resources folder. Because we're already using the "it" profile, this file will be picked up by Spring Boot and overwrite the datasource url.

spring:
  datasource:
    url: jdbc:tc:mysql:8.0:////my-app-test?serverTimezone=UTC&TC_REUSABLE=true
Enter fullscreen mode Exit fullscreen mode

Testcontainers can now do all the magic for us: the appropriate docker image with the given version is downloaded (if required) and started. This should ideally exactly match the version that is also used in production. The database "my-app-test" is created and our application context connects to it. Depending on how we initialize our database schema - for example with Flyway or Liquibase - this will be automatically applied upon our test database during context startup.

With the TC_REUSABLE flag, the Docker container is not automatically terminated at the end of the tests, but available for reusage without a longer waiting period. Occasionally, the container must be terminated manually to fully reset the database. To enable reuse, the file /<usersdir>/.testcontainers.properties must be extended by the entry testcontainers.reuse.enable=true.

This approach works well for all major databases, however as document-oriented databases are not based on JDBC, we can instead also extend our BaseIT class with the following code snippet:

private static final MongoDBContainer mongoDBContainer;

static {
    mongoDBContainer = new MongoDBContainer("mongo:5.0.8").withReuse(true);
    mongoDBContainer.start();
}

@DynamicPropertySource
public static void setDatasourceProperties(final DynamicPropertyRegistry registry) {
    registry.add("spring.data.mongodb.uri", mongoDBContainer::getReplicaSetUrl);
}
Enter fullscreen mode Exit fullscreen mode

  Testcontainers setup for a document-oriented database like MongoDB

This is basically doing the same thing as providing the application-it file: starting the given container and updating our application context to connect to it.

Running the tests

With this preparation, we can write our first test class. Let's assume that the following @RestController already exists. The referenced service returns all entries that exist in the "Test" table.

@RestController
@RequestMapping(value = "/api/tests", produces = MediaType.APPLICATION_JSON_VALUE)
public class TestController {

    private final TestService testService;

    @Autowired
    public TestController(final TestService testService) {
        this.testService = testService;
    }

    @GetMapping
    public List<TestDTO> getAllTests() {
        return testService.findAll();
    }

}
Enter fullscreen mode Exit fullscreen mode

  TestController of our Spring Boot app

We can now create a class TestControllerIT that extends our abstract base class. The test method sends a GET request to the existing endpoint using our TestRestTemplate.

public class TestControllerIT extends BaseIT {

    @Test
    @Sql({"/data/clearAll.sql", "/data/testData.sql"})
    public void getAllTests_success() {
        final HttpEntity<String> request = new HttpEntity<>(null, new HttpHeaders());
        final ResponseEntity<List<TestDTO>> response = restTemplate.exchange(
                "/api/tests", HttpMethod.GET, request, new ParameterizedTypeReference<>() {});

        assertEquals(HttpStatus.OK, response.getStatusCode());
        assertEquals(1, response.getBody().size());
        assertEquals((long)1000, response.getBody().get(0).getId());
    }

}
Enter fullscreen mode Exit fullscreen mode

  TestControllerIT of our Spring Boot app

Even though our database schema is already initialized, we still lack explicit test data that we need for our test. For this we use the Spring annotation @Sql, which executes two scripts and thus puts our database into a known state. In our case, there is now exactly one entry in the Test table that we expect as a result.

DELETE FROM test;
Enter fullscreen mode Exit fullscreen mode

  Wiping out all data with src/test/resources/data/clearAll.sql

INSERT INTO test (
    id,
    test
) VALUES (
    1000,
    'Aenean pulvinar...'
);
Enter fullscreen mode Exit fullscreen mode

  Create a single table entry with testData.sql

With restTemplate.exchange(...) we send a GET request to /api/tests. The functionality is basically the same as we already know from Spring's RestTemplate class. Our defined request does not send any data (null). The response is deserialized to type List<TestDTO> and in the assertions we check that the HttpStatus and the list match our expectation.

When we run the test for the first time, we have to wait a bit for the Docker container to start, depending on our environment. It may also be useful to already cache the image beforehand using docker pull mysql:8.0 to avoid issues with the download. Our test should now go through without errors.

Conclusion

With the setup described, we have created a way to check the behavior of our application including the database from a high level. With this we have a very good addition to our unit tests. Testcontainers also offers support for other services such as RabbitMQ, so we can flexibly extend BaseIT as needed.

In the Professional plan, Bootify offers the option to activate Testcontainers. This initializes the Spring Boot application including the described setup, depending on the selected database. It also generates the IT classes and scripts according to the tables and controllers created.

» See Features and Pricing
 

Further readings

Testcontainers homepage

Official guide on testing Spring Boot applications

Docker Hub for finding images

@Sql and @SqlMergeMode explanation

Where to look for the .testcontainers.properties

Discussion (0)