Aleix's Learnings

Share this post

Stateful Testcontainers for Spring Boot 3.1 Dev Mode

learnings.aleixmorgadas.dev
Technical

Stateful Testcontainers for Spring Boot 3.1 Dev Mode

Configuring containers to keep the state between executions to improve the DevEx. Using MongoDB as example.

Aleix Morgadas
Sep 6, 2023
1
Share this post

Stateful Testcontainers for Spring Boot 3.1 Dev Mode

learnings.aleixmorgadas.dev
Share

Spring Boot 3.1 introduced a lot of enhancements for DevEx. In particular, their integration with Testcontainers for test and DevMode.

DevMode

Now, you can declare your infrastructure dependencies and run your application in local machine by declaring via Spring Beans which Containers to use.

Something like:

package dev.aleixmorgadas;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.devtools.restart.RestartScope;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.context.annotation.Bean;
import org.testcontainers.containers.BindMode;
import org.testcontainers.containers.MongoDBContainer;
import org.testcontainers.utility.DockerImageName;

import java.util.List;


@TestConfiguration(proxyBeanMethods = false)
public class TestMyApplication {

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

    @Bean
    @RestartScope
    @ServiceConnection
    MongoDBContainer mongoContainer() {
        return new MongoDBContainer(DockerImageName.parse("mongo:6.0.8"));
    }
}

With this code, we are telling Spring to use a MongoContainer and replace the configuration with the Container one via `@ServiceConnection` annotation.

Shortly after using it you will find something frustrating.

Your database state disappeared after stopping the application.

The missing developer experience - Stateful containers

Compared to tests that you want to start clean on each execution, when developing in local you aim to keep the state between runs.

Here we have two options:

  1. Testcontainers reuse feature. It tells Testcontainers to not stop the container, and when we start again, it will reuse the same container instance.

  2. Add a volume.

1. Reuse container

Let’s declare the mongoContainer as a reusable with a simple flag.

@Bean
@RestartScope
@ServiceConnection
MongoDBContainer mongoContainer() {
    return new MongoDBContainer(DockerImageName.parse("mongo:6.0.8"))
           .withReuse(true);
}

Pro

  • The container will keep its state between executions.

Cons

  • If we restart our computer or just stop the container, we will lose the sate.

2. Adding a volume

MongoClient example

@Bean
@RestartScope
@ServiceConnection
MongoDBContainer mongoContainer() {
    return new MongoDBContainer(DockerImageName.parse("mongo:6.0.8")) {
            @Override
            public void configure() {
                withFileSystemBind("./.data", "/data/db");
                waitingFor(Wait.forLogMessage("(?i).*waiting for connections.*", 1));
            }

            @Override
            protected void containerIsStarted(InspectContainerResponse containerInfo, boolean reused) {
                // Disable default configuration due to replica set breaking configuration with stateful approach
            }
        };
}

Postgres example

@Bean
@RestartScope
@ServiceConnection
PostgreSQLContainer<?> postgresContainer() {
    return new PostgreSQLContainer<>(DockerImageName.parse("postgres:15"))
        .withFileSystemBind("./.data", "/var/lib/postgresql/data");
}

Pro

  • It keeps the state between executions and keeps the data after stopping the container.

Cons

  • You have state 🤷‍♂️.


At this point, we fixed the problem with the state, but if we are developing in local, we might need to access the database to check anything.

Accessing the database painlessly - Fixed Random Ports

Testcontainers was originally intended for tests (and lucky us, we are seeing that it applies to more areas than just tests), it has some design decisions meant for testing best practices.

One of those best practices are random ports.

From the host's perspective Testcontainers actually exposes this on a random free port. This is by design, to avoid port collisions that may arise with locally running software or in between parallel test runs.

This is a great decision from a Testing point of view, and I fully recommend following this approach. BUT, when I’m developing in local, I don’t want to change my DB Tool each time the port changes.

We have two options:

  1. Fixed ports

  2. Testcontainers Desktop

1. Fixed ports

This is the fastest to apply, into your DevMode Configuration.

@Bean
@RestartScope
@ServiceConnection
MongoDBContainer mongoContainer() {
    var container = new MongoDBContainer(DockerImageName.parse("mongo:6.0.8"));
    container.setPortBindings(List.of("55555:27017"));
    return container;
}

Pros

  • You don’t need to change your connection confirguration each time you start your application.

Cons

  • You might have port collitions.

2. Testcontainers Desktop

Testcontainers is well aware of this problem and they provided a solution for it via the Testcontainers Desktop.

You can see the full feature description here.

It added a new option called `Development Services` in the application.

Which comes with a lot of preconfigured modules which what they do is map the random port to the prefeered port of the infrastructure you are running.

Here an Redis example of how it is configured for you as default config.

Pros

  • You don’t need to change your connection confirguration each time you start your application.

Cons

  • You need to install Testcontainers Desktop. None 😄

1
Share this post

Stateful Testcontainers for Spring Boot 3.1 Dev Mode

learnings.aleixmorgadas.dev
Share
Comments
Top
New
Community

No posts

Ready for more?

© 2023 Aleix Morgadas
Privacy ∙ Terms ∙ Collection notice
Start WritingGet the app
Substack is the home for great writing