Local development with Testcontainers

Testcontainers is a very helpful library when it comes to integration testing.
One of the main benefits of it is that you can code your “dependencies” (like databases, brokers, cloud mocks, and other I/O sources) and start them with Docker from your tests - no need to run any additional command like docker-compose up or anything!

But sometimes it is still useful to start your application and play with it. This is why some projects still have a Docker Compose file next to the project.

Today I want to show you how you can reuse your existing testing infrastructure based on Testcontainers for local development.

What we already have

Let’s say we’re using Testcontainers already. Depending on your framework (see the framework-independent option at the bottom of this article), it may look somehow like this (here I am using Spring Boot):

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@ContextConfiguration(initializers = AbstractIntegrationTest.Initializer.class)
abstract class AbstractIntegrationTest {

   static class Initializer
         implements ApplicationContextInitializer<ConfigurableApplicationContext> {

      static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>();

      static GenericContainer<?> redis = new GenericContainer<>("redis:3-alpine")
          .withExposedPorts(6379);

      static KafkaContainer kafka = new KafkaContainer();

      public static Map<String, String> getProperties() {
         Startables.deepStart(Stream.of(redis, kafka, postgres)).join();

         return Map.of(
               "spring.datasource.url", postgres.getJdbcUrl(),
               "spring.datasource.username", postgres.getUsername(),
               "spring.datasource.password",postgres.getPassword(),

               "spring.redis.host", redis.getContainerIpAddress(),
               "spring.redis.port", redis.getFirstMappedPort() + "",
               "spring.kafka.bootstrap-servers", kafka.getBootstrapServers()
         );
        }

      @Override
      public void initialize(ConfigurableApplicationContext context) {
         var env = context.getEnvironment();
         env.getPropertySources().addFirst(new MapPropertySource(
               "testcontainers",
               (Map) getProperties()
         ));
      }
  }
}

Here we start PostgreSQL, Redis and Kafka, and set the context properties accordingly.

Note that ApplicationContextInitializer has nothing to do about testing, and can be applied to any (configurable) Spring context!

Which means that we can do…

Self Contained Applications

There are multiple approaches (args, system properties, Spring factories), but I would like to describe my favourite one.

Given the following (typical) Spring Boot main class:

@SpringBootApplication
public class DemoApplication {

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

We can change it a little and expose the application builder via createSpringApplication method:

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        createSpringApplication().run(args);
    }

    public static SpringApplication createSpringApplication() {
        return new SpringApplication(Application.class);
    }
}

So that now we can create a TestApplication class in the test source set next to our testing infrastructure that we will be using later for starting our app locally with all required dependencies:

public class TestApplication {
    public static void main(String[] args) {
        var application = Application.createSpringApplication();

        // Here we add the same initializer as we were using in our tests...
        application.addInitializers(new AbstractIntegrationTest.Initializer());
        
        // ... and start it normally
        application.run(args);
    }   
}

You can run it the same way you would run your “main” Application class, but now it will also start PostgreSQL, Redis & Kafka.

Making it fast

This approach works well if you need to start the app once and play with it, but, in case TDD is not your thing and you develop by starting the app, you may have noticed a few issues with this setup:

  1. It takes time to start databases, and you start them every time you run the app, unlike with Docker Compose
  2. Not really a separate issue, but the result of the first one - you always loose the state.
  3. Debugging the DB’s state is hard because the ports are always random

And, while the second can be solved by populating the DBs with some state, the question of speed remains open.
Not to mention that it does not feel the same as it used to be - some DB always running on a well known port.

All of that is happening because Testcontainers’ main focus is testing, and it makes sure that containers are terminated after you run your tests.

This is where another feature of Testcontainers comes in handy - Reusable Containers!

And, while it is still in preview, it works very well for our use case.

It takes a minor effort to start using it:

  1. add .withReuse(true)
  2. add .withNetwork(null) to Kafka (should be fixed soon)
  3. set testcontainers.reuse.enable=true in $HOME/.testcontainers.properties file

Once done, we can see that the containers (and their randomly selected ports) are preserved between the runs of our app 🎉

Another great side effect of this feature is that if you have multiple services that need the same version & configuration of a database, they will reuse it too! Something that you cannot achieve with Docker Compose ;)

Also, unlike with Docker Compose, it will never conflict with existing running containers because the ports will remaind randomized when you start the tests / app for the first time.

Make it even faster!

With reusable containers, everything starts fast already. But we still have to wait for a couple of seconds, plus a few more seconds to start our own app.

Can we do something about it? Of course! Spring Boot supports a very fast development mode that will restart only a subset of your app.

How to activate it? Just add testCompile 'org.springframework.boot:spring-boot-devtools' to your dependencies and you’re good to go!

Now, if we make a change to the already running app (and build id via our IDE’s Build action), Spring Boot will restart it, but it won’t run TestApplication.main again, and our containers will remain running & configured. s In fact, we don’t even have to use reusable containers, but they still make debugging easier by preserving the ports :)

Warp speed development! 🚀

Generic approach

So far we were talking about Spring Boot, but it does not mean that other frameworks cannot benefit from this “pattern”!

In fact, if we cannot touch the main class of our Spring Boot app, we may also want to use something more generic.

Luckily, almost all (if not all) JVM frameworks support configuration with either args or system properties.

We can use this to pass our properties to any JVM application:

public class TestApplication {
    public static void main(String[] args) {
        // "Initializer" here can be any class that defines & starts containers
        Initializer.getProperties().forEach(System::setProperty);

        // Just call the regular "public static void main" method here
        Application.main(args);
    }
}

Docker Compose is not the king anymore

As you can see, with just a few changes, you can start reusing the same container definitions you would use in your tests… but for local development too!

Next iterations of the Reusable Containers feature will also support terminating started containers after a certain timeout, so that if you’re not working on one of your services anymore, you won’t need to care about stopping the containers you started for it - everything will be done automagically for you. With one tool. How cool is that?

comments powered by Disqus