skip to Main Content

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?

3

Answers


  1. Chosen as BEST ANSWER

    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
        }
    }
    

  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
    
    Login or Signup to reply.
  3. 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

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search