Color Mode


    Language

Isolated Testing with Test Containers and Spring Boot in Kotlin

August 1, 2023

Are you using SpringBoot for backend services and looking to integrate TestContainers into your testing setup?

If so, then this example should give you an idea of how to achieve it in the shortest amount of time using the least possible configuration to set the implementation up. To do this we will be considering Isolated Testing as a use case and use the TestContainers to achieve this in the most simple way possible in order to run an integration test. This setup would be more suitable for the projects where integration tests have higher priority and are part of regular execution in the CI/CD pipeline.

Testing Lab

The purpose of isolated test, with or without TestContainers, is very important in Backend applications, because at some point the integration tests should be stable enough to make part of regular execution in CI/CD pipeline chain.

Besides, we want to make sure tests never interfere with each other during the execution and especially the technical setup does not enforce more and more refactoring over the time because of dependencies among the tests.

To ensure the correctness of the test results and the stability of each test method execution, we need to make sure the internal or external services used in the integration test context do not produce a data collision, and each test method execution can set up the data state for the service it requires. This could be done for example on an infrastructure premises by running separate instances of services on which our tests would depend. By doing so, we could achieve the goal, but not in an efficient way. This for example would require more effort to set it up and also to make sure that the test results are not impacting each other before, after and during the execution.

What could possibly go wrong?

What problems would we encounter with this setup? The earlier will be too expensive and the later one will is not flexible enough to prevent parallel test execution.

Of course, we could make sure the data is clean in the datasource before starting the tests to support parallel execution, but the cost of knowing the whole test system in order to modify or extend it would be a heavy task and never safe because of the risk of producing bugs within the test code.

Luckily, TestContainers would make this easy for us so that we do not need to worry about the data collision or parallel execution. Why? Depending on our setup, the technology would simply take care of it by creating and destroying the type of instance we require for our integration test either for each test case or test class. The execution in this manner might be a little more expensive, but the cost pays us with some values of easy setup, stability, flexibility and independence in tests. Besides, making sure that both old and new tests written at any point in time are still independent of each other could save lots of refactoring time.

How to design an integration test?

With new releases of SpringBoot and TestContainers it is way easier to accomplish the integration. Depending on the use case, as in unit testing, as well as in the integration test, the recommended structure to use in the test code is the classical testing strategy based on GIVEN/WHEN/THEN concept as following:

  1. Given: Data Preparation: Prepares or mocks the test data. .

  2. When: Target Code: Executes the testable production code.

  3. Then: Assertion: Checks the expected data.

What is different here compared to unit testing is that we not only need to prepare the test data but also, depending on the requirements, provide external and/or internal services for the test setup, allowing each test case to manage them individually according to their needs. For example, if our integration test depends on a DB instance, then we would need to provide a Docker based DB instance before the test execution and insert the necessary data.


@RestController
@RequiredArgsConstructor
internal class UserController(val service: UserService) {

    @GetMapping("/users")
    fun all(): Iterable<User> {
        return service.findAll()
    }

    @PostMapping("/users")
    fun newUser(@RequestBody newUser: User): User {
        return service.createNew(newUser)
    }

    @GetMapping("/users/{id}")
    fun one(@PathVariable id: Long): User? {
        return service.getUser(id) ?: throw UserNotFoundException(id)
    }
}

This specific example has been developed as a basic SpringBoot application which exposes endpoints to insert and fetch basic data from PostgreSQL datasource. If we were to write multiple integration tests for this application we could either run multiple PSQL instances by creating and destroying them per test case or class or use 1 instance and prepare the data state before and after each execution.

@Testcontainers
@SpringBootTest(classes = [IsolatedTestingApplication::class])
@ActiveProfiles("test")
@AutoConfigureMockMvc(addFilters = false)
class UserApiIT {

    @Autowired
    var userRepository: UserRepository? = null

    @Autowired
    var mockMvc: MockMvc? = null

    @Test
    fun `Retrieve User from DB via the Endpoint`() {
        //GIVEN        
        val user = User("Name", "Lastname")
        userRepository?.save(user)

        //WHEN
        mockMvc!!.perform(
            MockMvcRequestBuilders.get("/users/{id}", 1))
            
        //THEN
                .andExpect(MockMvcResultMatchers.status().isOk)
                .andExpect { result: MvcResult ->
                    val content = result.response.contentAsString
                    assertThat(content).contains("Lastname")}
                .andReturn())
    }
}

In the test case, the requests will be made by using Spring Mock MVC to reach the PSQL Datasource, which we can then set up by using Spring Boot configuration file and TestContainers provided PostgreSQL Docker based instance.

Next, the configuration would make sure that the SpringBoot and TestContainer integration could set up and run a test application before the test case execution:

appication.yml

spring:
  datasource:
    url: jdbc:postgresql://${DATABASE_HOST:localhost}: ${DATABASE_PORT:5435}/${DATABASE_NAME:template}
    username: ${DATABASE_USER:templateUser}
    password: ${DATABASE_PASSWORD:templatePassword}

appication-test.yml

spring:
  datasource:
    url: jdbc:tc:postgresql:14:///template

This will automatically initiate an PSQL Docker based instance by creating the mentioned user as well as the database in it. As soon as the Docker instance is ready SpringBoot application startup will be triggered along with the FlyWay migration scripts to be executed. Based on the given configuration the Docker PSQL instance will have the following setup ready for the application to connect and use...

  • Host: localhost
  • Username: templateUser
  • Password: templatePassword
  • Database: template
Docker and Containers in action

While running the provided test sample you can simply follow in the Docker Desktop that 2 Docker Containers are appearing and disappearing on the fly to support our test execution on.


Technical Setup

The source code for the described use case is available in this GitHub repository provided by Monstarlab:: Having Docker Desktop installed is a pre-requisite to run it.


Executing the following command in CLI should be sufficient to see and check the test results.

./gradlew clean build


The following steps are necessary to run the backend application in a CLI:

  1. Initiate PSQL Docker Container with a Database.
docker run -d --name template_postgres -e POSTGRES_USER=templateUser -e POSTGRES_PASSWORD=templatePassword -e POSTGRES_DB=template -p 5435:5432  --restart=always postgres
  1. Start the backend application
./gradlew bootRun

The choice of Kotlin as a language and Gradle as a build tool is merely one step forward towards the modern programming world and has no impact on the setup: this could have been easily also achieved with Maven and Java.

Tools & References used here...

  • Monstarlab:: template with Kotlin and Gradle
  • Gradle as build tool with KTS
  • Kotlin as a programming language
  • Hibernate as an ORM layer for JPA
  • PostgreSQL as a Datasource
  • FlyWay for DB script migrations
  • Spring Boot, Note that since 3.1 TestContainers are supported by SrpingBoot
  • Test Containers for running PSQL Docker based Container
  • Unsplash Images for writing the blog post
  • Brain and Mind from Monstarlab::
kotlingradlebackendintegration testspring boottestcontainersisolated testing

Author

Azat Martirosyan

Azat Martirosyan

Director of Java & Kotlin

Passionate about Engineering and Natural Sciences

You may also like

November 7, 2024

Introducing Shorebird, code push service for Flutter apps

Update Flutter apps without store review What is Shorebird? Shorebird is a service that allows Flutter apps to be updated directly at runtime. Removing the need to build and submit a new app version to Apple Store Connect or Play Console for review for ev...

Christofer Henriksson

Christofer Henriksson

Flutter

May 27, 2024

Introducing UCL Max AltPlay, a turn-by-turn real-time Football simulation

At this year's MonstarHacks, our goal was to elevate the sports experience to the next level with cutting-edge AI and machine learning technologies. With that in mind, we designed a unique solution for football fans that will open up new dimensions for wa...

Rayhan NabiRokon UddinArman Morshed

Rayhan Nabi, Rokon Uddin, Arman Morshed

MonstarHacks

ServicesCasesAbout Us
CareersThought LeadershipContact
© 2022 Monstarlab
Information Security PolicyPrivacy PolicyTerms of Service