DEV Community

Thomas
Thomas

Posted on • Edited 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 our 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 in our Spring Boot context it would be great to examine the running app including the database (for example MySQL 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 start 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 start by adding the required dependencies.

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

  Adding dependencies to our build.gradle

Besides spring-boot-starter-test we also add org.springframework.boot:spring-boot-testcontainers. Since Spring Boot 3.1.0 there is first-class support for Testcontainers, making our live even easier. With org.testcontainers:mysql we add the container class for the actual database we are using - 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 will inherit later on. The first version of this class looks like this:

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

    @Autowired
    public MockMvc mockMvc;

}
Enter fullscreen mode Exit fullscreen mode

  First version of BaseIT.java

By using the @SpringBootTest annotation, Spring Boot will load the full application context and thus enables us to make good integration tests. 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, so during testing there shouldn't be any conflict. MockMvc is configured automatically with the annotation AutoConfigureMockMvc - there are a lot of useful helpers included for calling our endpoints.

Mocking the database

Now we want to configure a database that is exclusively available for our tests. For this we extend our BaseIT.java with the following code.

public abstract class BaseIT {

    // ...

    @ServiceConnection
    private static final MySQLContainer mySQLContainer = new MySQLContainer("mysql:8.0");

    static {
        mySQLContainer.withUrlParam("serverTimezone", "UTC")
                .withReuse(true)
                .start();
    }

}
Enter fullscreen mode Exit fullscreen mode

  Testcontainers setup to provide MySQL as a container

The proper container from Testcontainers is initialized with the given image "mysql:8.0", in our case the newest version of MySQL. Ideally this should exactly match the version that is also used in production.

Originally a special method annotated with @DynamicPropertySource was required to provide the connection credentials to the application context. With Spring Boot 3.1.0 that's not required anymore for most databases and simply adding @ServiceConnection will do exactly that.

Within the static block we are setting the reuse parameter, so that the container will continue running until we manually stop it. Usually that's not causing issues and makes testing the application faster - the Docker container can be stopped manually if required. To enable reuse, the file /<usersdir>/.testcontainers.properties in your environment must be extended by the entry testcontainers.reuse.enable=true.

Finally we are starting the container with a call to start(). This approach ensures that all extending classes will reuse the same container. Depending on how we initialize our database schema - for example with Flyway or Liquibase - this will be automatically applied to our test database within the container during context startup.

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

  Example endpoint in 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 MockMvc. We're asserting that the HTTP status and the response JSON are matching our expectations.

public class TestControllerIT extends BaseIT {

    @Test
    @Sql({"/data/clearAll.sql", "/data/testData.sql"})
    public void getAllTests_success() throws Exception {
        mockMvc.perform(get("/api/tests")
                        .accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.length()").value(1))
                .andExpect(jsonPath("$[0].id").value(((long)1000)));
    }

}
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

Docker must be available and running when the test is executed. When we run the test for the first time we have to wait a bit for our container to be available. It may 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 useful addition to our unit tests. Testcontainers also offers support for other services such as RabbitMQ, so we can flexibly add such containers to 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 REST 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

Top comments (0)