7

I'm doing integration tests in my Spring Boot app. The app needs a Redis to work with.

During development phase, I have a local container of Redis that the app connects to.

For the integration tests, I'm using testcontainers and I also followed their example of how to use a Redis container.

At some point I understood that the test run correctly only when the development container was up and running. If it is down, the integration tests are falling because they can't reach Redis.

So the integration test class looks like that:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = SharkApplication.class,
    webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestPropertySource(locations = "classpath:application-integrationtests.yml")
@AutoConfigureMockMvc
public class SharkIntegrationTest {
static GenericContainer redis = new GenericContainer("redis:3.0.6")
        .withExposedPorts(6379);

@BeforeClass
public static void before(){
    redis.start();
}

@AfterClass
public static void after(){
    redis.stop();
}
...

When running the test, I can see this in the log:

14:36:24.372 [main] DEBUG  [redis:3.0.6] - Starting container: redis:3.0.6
14:36:24.372 [main] DEBUG  [redis:3.0.6] - Trying to start container: 
   redis:3.0.6
14:36:24.373 [main] DEBUG  [redis:3.0.6] - Trying to start container: 
    redis:3.0.6 (attempt 1/1)
14:36:24.373 [main] DEBUG  [redis:3.0.6] - Starting container: redis:3.0.6
14:36:24.373 [main] INFO  [redis:3.0.6] - Creating container for image: 
   redis:3.0.6
...
14:36:25.282 [main] INFO  [redis:3.0.6] - Container redis:3.0.6 started

But then the app fails as it can't reach Redis:

Caused by: redis.clients.jedis.exceptions.JedisConnectionException: java.net.ConnectException: Connection refused: connect

At some point I tried to change the port on which the container should start. From 6379 to 16379 (changed both in the code and in the yml file), but then the test is entering an endless loop and prints to the screen:

14:41:57.258 [ducttape-0] DEBUG org.testcontainers.containers.ExecInContainerPattern - /amazing_beaver: Running "exec" command: /bin/bash -c </dev/tcp/localhost/16379 && echo

riorio
  • 6,500
  • 7
  • 47
  • 100
  • You can use @DynamicPropertySource to dynamically(DynamicPropertyRegistry registry) add host/port. – Hamedz Sep 29 '21 at 09:59

2 Answers2

11

You're missing a very important aspect of Testcontainers - random ports.

From the link you mentioned:

For example, with the Redis example above, the following will allow your tests to access the Redis service:
String redisUrl = redis.getContainerIpAddress() + ":" + redis.getMappedPort(6379);

Testcontainers starts everything with random ports to avoid conflicts.

You can follow this workshop to integrate it properly.

bsideup
  • 2,845
  • 17
  • 21
  • OK, I understand what you are saying. Do you also know how do you inject this `mappedPort` to the app during test time? I asked the same questions in a different thread here : https://stackoverflow.com/questions/50894302/inject-override-a-properties-value-to-a-spring-boot-properties-file – riorio Jun 17 '18 at 06:54
  • Yes, please check the link I posted. static block + setting properties so far the best way to do it. We (Testcontainers) will change our Spring Boot example to use this approach soon. – bsideup Jun 18 '18 at 07:59
  • 1
    How about the RedisClient, it;s only the Redis Server right? So in other to connect to Redis Server, we need to create a Redis Client with the connectionString as `redis://redisContainer.getContainerIpAddress() + redisContainer.getMappedPort(REDIS_PORT)` – Nguyễn Đức Tâm Aug 31 '21 at 06:39
8

When you declare a container this way:

static GenericContainer redis = new GenericContainer("redis:3.0.6")
    .withExposedPorts(6379);

You are telling TestContainers to map a random host port to the container port 6379. As shown in the following screenshot, for example, TestContainers mapped from host port 32881 to container port 6379:

docker ps

To access the Redis container in a test, you need to use the random host port, not the redis port 6379. To do so, you need to override (in runtime) the configuration values defined in application.properties to use the random host port.

Here is how you can do it:

package some.random.packagee;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.support.TestPropertySourceUtils;
import org.testcontainers.containers.GenericContainer;

@SpringBootTest
@ContextConfiguration(initializers = some.random.packagee.AbstractContainerBaseTest.Initializer.class)
public class AbstractContainerBaseTest {

    private static final int REDIS_PORT = 6379;

    // Optional
    @Autowired
    private RedisTemplate redisTemplate;

    // Optional 
    protected void cleanCache() {
        redisTemplate.getConnectionFactory().getConnection().flushAll();
    }

    public static class Initializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {

        static GenericContainer redis = new GenericContainer<>("redis:6-alpine")
            .withExposedPorts(REDIS_PORT)
            .withReuse(true);

        @Override
        public void initialize(ConfigurableApplicationContext context) {
            // Start container
            redis.start();

            // Override Redis configuration
            String redisContainerIP = "spring.redis.host=" + redis.getContainerIpAddress();
            String redisContainerPort = "spring.redis.port=" + redis.getMappedPort(REDIS_PORT); // <- This is how you get the random port.
            TestPropertySourceUtils.addInlinedPropertiesToEnvironment(context,  redisContainerIP, redisContainerPort); // <- This is how you override the configuration in runtime.
        }
    }
}

Then you extend the class AbstractContainerBaseTest in the classes that require to use Redis, for example:

package some.random.packagee;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;

class CacheTest extends AbstractContainerBaseTest {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @AfterEach
    void tearDown() {
        cleanCache();
    }

    @Test
    public void testSomeMethodUsingRedis() {
        // Add your test here.
    }
}
Julian Espinel
  • 2,586
  • 5
  • 26
  • 20