Spring Boot 3.1: Testcontainers Support

The Testcontainers framework is a great tool to help you write integration tests for your Spring Boot applications. You can easily start all dependencies of your application, like a database, a web server or a message broker. The Testcontainers framework let’s you specify these dependencies as code that can be checked in and makes it easily available for all developers that work on the code base of an application.

With the release of Spring-Boot 3.1, the Spring team introduced a new Spring Boot starter for Testcontainers, which makes it even easier to integrate the framework in your tests.

Spring Boot Demo Application

We will create the base application with the Sping Initializr and the following parameters:

  • Java Version 20
  • Maven as the build tool
  • Spring Boot Version 3.1

As for the dependencies, we will pick the following Spring Boot modules:

Just use any values you like for the project metadata and press the generate button create and download your project. Finally, unzip the files into any folder and open your favorite IDE to edit the project.

Looking at the generated pom file, we can see the Testcontainers dependencies and a specific dependency for a MongoDB Testcontainer. The Spring Initializr was smart enough to notice that we added the spring-data-mongodb module to our project and hence automatically add the required Testcontainers dependency to our pom. This mechanism of the Initializr works for all major databases and message brokers that have their own Spring Boot starter. For a full list of all products that are supported by the Testcontainers framework, check out this list.

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-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>mongodb</artifactId>
	<scope>test</scope>
</dependency>

Next, let’s create a simple REST endpoint that we can test with the new Testcontainers support. For this example, I will use a simple endpoint that returns a list of books which are read from the MongoDB.

@RestController
public class BookController {

    @Autowired
    private BookRepository bookRepo;

    @GetMapping
    @RequestMapping("/books")
    public Iterable<Book> getBooks() {
        return bookRepo.findAll();
    }
}

The BookController has the BookRepository as a dependency to query the book data from the MongoDB. The BookRepository is a simple Spring-Data-MongoDB repository. Furthermore, we need the Book class to deserialize the data from the database and return it to the client.

Here is the code for the repository and the domain object:

import org.springframework.data.mongodb.repository.MongoRepository;

public interface BookRepository extends MongoRepository<Book, Integer> {}
@Document
public record Book(@Id Integer id, String name, String author) {}

Next, we will modify the application main class to create some dummy data that we can later use in our tests. For this, we create a CommandLineRunner that uses the MongoDB repository to inserts a bunch of Book objects into the database:

@SpringBootApplication
public class Application {

  public static void main(String[] args) {
    SpringApplication.run(Application.class, args);
  }	

  @Bean
  public CommandLineRunner demo(BookRepository bookRepo) {
    return args -> {
      bookRepo.deleteAll();
      bookRepo.insert(new Book(1,"Lord Of the Rings", "J.R.R. Tolkien"));      
      bookRepo.insert(new Book(2,"Lord of Flies", "William Golding"));
      bookRepo.insert(new Book(3,"1984", "George Orwell"));
    };
  }
}

I don’t have a MongoDB running locally and we did not configure any connection details for it in a property file. We will provide the MongoDB for the application in the next section by using a Testcontainer.

Testcontainers with Spring Boot 3.1

If you start the above demo application it will fail because of the missing MongoDB that is used by the BookRepository.

Spring Boot 3.1 gives you two approaches to provide the database for testing your application.

  • You can start the application by a custom application starter class that spins up the database when the application is started with it.
  • You can write Spring Boot integration tests that start the required database before it is executed.

By the first approach, we can start and use our application without the need to install a database after the checkout.

Since the application should only be started with a test database when you develop it on your local machine or maybe on your CI/CD system but not in production, we cannot change the configuration of the application in the regular code base, but only in the test code. To do this, we create a second application starter class in the test code base and put it into the root package of your application.

Here is the code for a test starter class with a MongoDB test container configured:

@TestConfiguration(proxyBeanMethods = false)
public class TestApplicationStarter {

  @Bean
  @ServiceConnection
  MongoDBContainer mongoDbContainer() {
    return new MongoDBContainer("mongo:latest").withReuse(true);
  }

  public static void main(String[] args) {
    SpringApplication.from(ApplicationStarter::main)
    .with(TestApplicationStarter.class)
    .run(args);
  }
}

In line (1) we use the @TestConfiguration annotation to tell Spring that we want to do some additional configuration in this class, but only for testing purposes. The real magic happens next in line (4)-(8). We create a bean of type MongoDBContainer which is part of the Testcontainers framework. We pass a String to the constructor to specify which version of the Docker image we want to use. In the background this will download the specified version of MongoDB from Dockerhub and start that image.

Next, we need to add the new @ServiceConnection annotation to the bean definition in line (5). This new Spring annotation will figure out the connection details of the MongoDB instance and automatically update the connection parameters in our Spring Boot configuration.

Finally, we declare a main method to start the application in line (10) to (13). Here we use the new from method of SpringApplication to start the application with the configuration of the main code base. After that, we use the new with method to pass in an additional configuration class that will be applied on top of the base configuration. In our case we add the TestApplicationStarter that includes our MongoDB configuration from line (4) to (8). Of course you can put the Testcontainer configuration in a separate class, but I just use one class for this example to keep it simple

If we now use the TestApplicationStarter to start the application, a MongoDB will be started by the Testcontainers framework followed by the normal configuration of the main code base. After the start you should be able to query the list of books by hitting http://localhost:8080/books with a GET request.

Integration Tests with Testcontainers

Next, we will have a look how we can use the Testcontainer inside a Spring Boot integration test.

Let’s write with a simple integration test against the Books http endpoint to check if the dummy data is available.

@SpringBootTest(  webEnvironment = WebEnvironment.RANDOM_PORT ) 
class SpringBoot31TestcontainerSupportApplicationTests {

  @LocalServerPort
  int port;

  @Test
  public void testBookRepo() {
    RestTemplate template = new RestTemplate();
    Book[] books = template.getForObject("http://localhost:"+port+"/books"
      , Book[].class);
    Assert.assertEquals(3, books.length);
  }
}

In this test, I hit the GET endpoint of the application with a request and check that exactly three books are returned. This test will fail when executed, since there is no MongoDB running in the background when we start the test.

Let’s add the MongoDB instance for the test as a Testcontainer.

Here is the modified code, this time with the MongoDBContainer specified:

@SpringBootTest(  webEnvironment = WebEnvironment.RANDOM_PORT ) 
@Testcontainers
class SpringBoot31TestcontainerSupportApplicationTests {

   @LocalServerPort
   int port;

  @ServiceConnection
  @Container
  static MongoDBContainer container = new MongoDBContainer("mongo:latest");

  @Test
  public void testBookRepo() {
    RestTemplate template = new RestTemplate();
    Book[] books = template.getForObject("http://localhost:"+port+"/books",
       Book[].class);
    Assert.assertEquals(3, books.length);
  }
}

I added the @Testcontainers annotation on the class level in line (2) to tell the framework that containers are required for this test. The definition of the MongoDBContainer instance in line (7) to (9) is quite similar to the one we used for our test application starter class in the last section. We create a member variable of type MongoDBContainer and pass in the image version we want to use as a String. By declaring the member variable static, we specify that this instance should be reused for all tests in the class. Because we have only one test, this doesn’t matter in our case.

Since, the integration test is not a configuration class, we cannot use the @Bean annotation on our member variable. To fix this, we add the @Container annotation to the it to tell the Testcontainers framework that the MongoDBContainer should be started and stopped automatically with the test by the framework. Finally, the connection properties for the database are again updated by the @ServiceConnection annotation in line (7).

If we now run the test, it will start a MongoDB instance before the test and register it in the application. After the test, the container is shut down automatically.

You can see that is it just a few lines of code to start a dependency of your application and test it with real dependencies running. There is very little reason anymore to use in-memory DBs or mock away dependencies when you can simply create them that easy.

If you want to check out these examples, you can find the code on github.


Beitrag veröffentlicht

in

, , ,

von

Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert