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')
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;
}
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();
}
}
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();
}
}
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)));
}
}
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;
Wiping out all data with src/test/resources/data/clearAll.sql
INSERT INTO test (
id,
test
) VALUES (
1000,
'Aenean pulvinar...'
);
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.
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)