3

I am writing test for my service in spring boot

@Component
public class MyService {
    @Autowired
    StringRedisTemplate stringRedisTemplate;
    // a number of other @Autowired dependencies

    public User getUser(String uuid) {
        var key = String.format("user:%s", uuid);
        var cache = stringRedisTemplate.opsForValue().get(key);
        if (cache == null) {
            // return user from database
        } else {
            // return user from deserialized cache
        }
    }
}

@Testcontainers
@SpringBootTest
class MyServiceTest {
    @Autowired
    StringRedisTemplate stringRedisTemplate;
    @Autowired
    MyService myService;
    
    @Container
    public static GenericContainer<?> redis =
        new GenericContainer<>("redis:5.0.14-alpine3.15").withExposedPorts(6379);

    @BeforeClass
    public static void startContainer() {
        redis.start();
        var redisUrl = String.format("redis://%s:%s", redis.getHost(), redis.getMappedPort(6379));
        System.setProperty("spring.redis.url", redisUrl);
    }

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

    @Test
    void getUser_returnCachedUser() {
        // breakpoint here
        stringRedisTemplate.opsForValue().set("user:some-uuid", "{\"uuid\":\"some-uuid\",\"name\":\"cache\"}");
        var user = myService.getUser("some-uuid");
        assertEquals("cache", user.name);
    }
}

When I ran this in debug mode and hit breakpoint, I noticed port redis.getMappedPort(6379) in console was not equal to stringRedisTemplate.connectionFactory.client or myService.stringRedisTemplate.connectionFactory.client.

Did System.setProperty overwrite properties and take effect in this case? How can I use testcontainers in spring boot integration test?

Mike
  • 173
  • 4
  • 14
  • with the TestContainers and Container annotations you are letting test containers manage the lifecycle but here you have your own dedicated start and stop methods. Possibly this could one of the problems. – Daniel Jacob Feb 11 '22 at 12:47

3 Answers3

2

I'd suggest to use slightly different container from playtika which is built on top of testcontainers.

What you need to do is to include spring-cloud-starter-bootstrap in your pom.xml (it's enough as a test dependency).

and then in your test application.yaml|properties use following:

spring:
  redis:
    port: ${embedded.redis.port}
    password: ${embedded.redis.password}
    host: ${embedded.redis.host}
    ssl: false
bilak
  • 4,526
  • 3
  • 35
  • 75
1

You can use getFirstMappedPort() rather than redis.getMappedPort(6379) because testcontainer uses random ports. 6379 is host machine port, but the redis container's port is randomly assigned to avoid conflicts. More details could be found in another thread: https://stackoverflow.com/a/50869731

batur
  • 46
  • 6
0

I prepared 2 templates in Java and Kotlin, which are working for me (servlet not webflux), for your reference.

// AbstractIntegrationTest.java
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.utility.DockerImageName;

@SpringBootTest
@Testcontainers
@DirtiesContext
public abstract class AbstractIntegrationTest {
    public static final GenericContainer<?> redis =
        new GenericContainer<>(DockerImageName.parse("redis:5.0.14-alpine3.15"))
            .withExposedPorts(6379);

    public static final GenericContainer<?> mysql =
        new GenericContainer<>(DockerImageName.parse("mysql:5.7.28"))
            .withExposedPorts(3306)
            .withEnv("MYSQL_ROOT_PASSWORD", "password")
            .withEnv("MYSQL_USER", "test")
            .withEnv("MYSQL_PASSWORD", "password")
            .withEnv("MYSQL_DATABASE", "db_test");

    static {
        redis.start();
        mysql.start();
    }

    @DynamicPropertySource
    static void registerProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.redis.url", () -> String.format(
            "redis://%s:%s",
            redis.getHost(),
            redis.getMappedPort(6379)
        ));
        registry.add("spring.datasource.url", () -> String.format(
            "jdbc:mysql://%s:%s@%s:%s/%s",
            "root",
            "password",
            mysql.getHost(),
            mysql.getMappedPort(3306),
            "db_test"
        ));
    }
}

// MyServiceTest.java
@Testcontainers
@SpringBootTest
@DirtiesContext
class MyServiceTest extends AbstractIntegrationTest {
    @Autowired
    MyService myService;

    @Test
    void someMethod() {
        // preparation

        // invocation
        myService.someMethod()

        // assertion
    }
}

Here is the Kotlin version:

// AbstractIntegrationTest.kt
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.test.annotation.DirtiesContext
import org.springframework.test.context.DynamicPropertyRegistry
import org.springframework.test.context.DynamicPropertySource
import org.testcontainers.containers.GenericContainer
import org.testcontainers.junit.jupiter.Testcontainers
import org.testcontainers.utility.DockerImageName

@SpringBootTest
@Testcontainers
@DirtiesContext
abstract class AbstractIntegrationTest {
    companion object {
        @JvmStatic
        val redis: GenericContainer<*> = GenericContainer(DockerImageName.parse("redis:5.0.14-alpine3.15"))
            .withExposedPorts(6379)

        @JvmStatic
        val mysql: GenericContainer<*> = GenericContainer(DockerImageName.parse("mysql:5.7.28"))
            .withExposedPorts(3306)
            .withEnv("MYSQL_ROOT_PASSWORD", "password")
            .withEnv("MYSQL_USER", "test")
            .withEnv("MYSQL_PASSWORD", "password")
            .withEnv("MYSQL_DATABASE", "db_test")

        init {
            redis.start()
            mysql.start()
        }

        @DynamicPropertySource
        @JvmStatic
        fun registerProperties(registry: DynamicPropertyRegistry) {
            registry.add("spring.redis.url") {
                String.format(
                    "redis://%s:%s",
                    redis.host,
                    redis.getMappedPort(6379)
                )
            }
            registry.add("spring.datasource.url") {
                String.format(
                    "jdbc:mysql://%s:%s@%s:%s/%s",
                    "root",
                    "password",
                    mysql.host,
                    mysql.getMappedPort(3306),
                    "db_test"
                )
            }
        }
    }
}

// MyServiceTest.kt
@SpringBootTest
internal class MyServiceTest : AbstractIntegrationTest() {
    @Autowired
    lateinit var myService: MyService

    @Test
    fun someMethod() {
        // preparation

        // invocation
        myService.someMethod()

        // assertion
    }
}
Mike
  • 173
  • 4
  • 14