Simplify Local Development with Testcontainers

In this blog post, I will explain how you can you can reuse your existing testing infrastructure based on Testcontainers for local development.

In my previous blog posts ( part-1, part-2), I have explained how we can use docker-compose to start containers and use them for local development.

If you are using Testcontainers library for your integration tests, with small refactorting of your code, you can start the required containers with Testcontainers for local development with out using the docker compose file.

For demonstration purpose, I am going to use the rabbitmq-producer-testcontainers application from the GitHub.

I am going to refactor the integration test classes so that we can use can move the containers starting logic to common class which can be used by both local development and integration tests.

Step 1) Moving all the container initializations to common classes

Referenced repo has following code for running the integration tests.

@Testcontainers
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@DirtiesContext
@Slf4j
public class RabbitMQProducerControllerIT {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Autowired
    private RabbitAdmin rabbitAdmin;

    @Autowired
    private RabbitMQProducerController rabbitMQProducerController;

    @Autowired
    private TestRestTemplate testRestTemplate;

    static RabbitMQContainer rabbitMQContainer ;


    static {
        rabbitMQContainer = new RabbitMQContainer("rabbitmq:3.10.6-management-alpine")
                .withStartupTimeout(Duration.of(100, SECONDS));

        rabbitMQContainer.start();

    }Code language: Java (java)

Let’s refactor the class so that we can centralize the container initialization logic. We are going to use this class for running integration tests and local development.

@Testcontainers
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Slf4j
@ContextConfiguration(initializers = AbstractIntegrationTest.Initializer.class)
public abstract class AbstractIntegrationTest {

	public static  class Initializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
		
		static final RabbitMQContainer rabbitMQContainer = new RabbitMQContainer("rabbitmq:3.10.6-management-alpine")
                .withStartupTimeout(Duration.of(100, SECONDS))
                .withEnv("JAVA_OPTS", "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=0.0.0.0:8005");

		
		 public static  Map<String, String> getProperties() {
			 Startables.deepStart(Stream.of(rabbitMQContainer)).join();
						 
			 return Map.of(
					  "spring.rabbitmq.host",rabbitMQContainer.getHost(),
				      "spring.rabbitmq.port", rabbitMQContainer.getAmqpPort().toString()



		 }
		

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



	}

	

}Code language: Java (java)

In our actual integration test we simply extend the abstract class

@Slf4j
public class RabbitMQProducerControllerIT  extends AbstractIntegrationTest{
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Autowired
    private RabbitAdmin rabbitAdmin;

    @Autowired
    private RabbitMQProducerController rabbitMQProducerController;

    @Autowired
    private TestRestTemplate testRestTemplate;
    
     ....
}Code language: Java (java)

Step 2) Refactoring the Spring Boot Main class.

We can refactor Spring Boot Main class a little and expose the application builder via createSpringApplication method

@SpringBootApplication
public class SpringbootRabbitmqProducerApplication {


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

	public static SpringApplication createSpringApplication() {
		return new SpringApplication(SpringbootRabbitmqProducerApplication.class);
	}Code language: Java (java)

Step 3) Write a Local Development class

Next we write local development class which will start application and required containers

public class LocalDevelopment {
	
	public static void main(String[] args) {
		
		SpringApplication application = SpringbootRabbitmqProducerApplication.createSpringApplication();
			
		application.addInitializers(new AbstractIntegrationTest.Initializer());

	        application.run(args);
	}
	

}
Code language: Java (java)

Now If you run the Localdeveloment class you can start application and test it from your local machine.

Debugging Applications

When we use docker-compose, we run the containers on dedicated port, so we can view the Data in database using database client and message posted to RabbitMQ using its management dashboard.

One disadvantage of running containers with Testcontainers library is every time we start containers they use the random port which makes it difficult to debug application or view the data

To overcome the random port problem, we can use the Proxy service to expose containers through static post.

Let’s see how it can be done. There are 2 libraries which you can use.

Option1 ) Using JavaNioTcpProxy

Add following library to pom xml

		<dependency>
			<groupId>com.github.terma</groupId>
			<artifactId>javaniotcpproxy</artifactId>
			<version>1.5</version>
		</dependency>Code language: Java (java)

In your Application initializer class, you start proxy server and map the random port to fixed port.

public static  class Initializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
		
		static final RabbitMQContainer rabbitMQContainer = new RabbitMQContainer("rabbitmq:3.10.6-management-alpine")
                .withStartupTimeout(Duration.of(100, SECONDS))
                .withEnv("JAVA_OPTS", "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=0.0.0.0:8005");	
		
		

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

			TcpProxyConfig  config = new StaticTcpProxyConfig(
			        5005,
			        rabbitMQContainer.getHost(),
			        rabbitMQContainer.getMappedPort(15672)
			);
			config.setWorkerCount(1);
			TcpProxy tcpProxy = new TcpProxy(config);
			tcpProxy.start();
			
		} 

	}
Code language: Java (java)

Based on above code, RabbitMQ management console is available at port 5005 when you start the application using LocalDevelopment class.

Option 2) Using TcpTunnel

Add library to pom xml

		<dependency>
			<groupId>net.kanstren.tcptunnel</groupId>
			<artifactId>tcptunnel</artifactId>
			<version>1.2.0</version>
		</dependency>Code language: Java (java)

In your Application initializer class, you start proxy server and you map the random port to fixed port.

public abstract class AbstractIntegrationTest {

	public static  class Initializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
		...		

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


			

			   Params params = new Params(5006, rabbitMQContainer.getHost(), rabbitMQContainer.getMappedPort(15672));
			    //we want to use the captured data in testing, so enable logging the tunnel data in memory with buffer size 8092 bytes
			    params.enableInMemoryLogging(8092);
			    
			    InMemoryLogger upLogger = params.getUpMemoryLogger();
			    
			    InMemoryLogger downLogger = params.getDownMemoryLogger();
			    //this is how we actually start the tunnel
			    net.kanstren.tcptunnel.Main main = new net.kanstren.tcptunnel.Main(params);
			    main.start();
	         
		} 

	}

	

}Code language: Java (java)

Based on above code, RabbitMQ management console is available at port 5006 when you start the application using LocalDevelopment class.

You can download the source code for the blog post from GitHub

References

https://bsideup.github.io/posts/local_development_with_testcontainers/

Similar Posts