There's a bit of ceremony involved at the moment:

@Testcontainers
@ContextConfiguration(initializers = DataRedisTestIntegrationTests.Initializer.class)
@DataRedisTest
public class DataRedisTestIntegrationTests {

    @Container
    public static RedisContainer redis = new RedisContainer();

    // …

    static class Initializer
            implements ApplicationContextInitializer<ConfigurableApplicationContext> {

        @Override
        public void initialize(
                ConfigurableApplicationContext configurableApplicationContext) {
            TestPropertyValues.of("spring.redis.port=" + redis.getMappedPort())
                    .applyTo(configurableApplicationContext.getEnvironment());
        }

    }

It would be nice to be able to do something like this instead:

@Testcontainers
@DataRedisTest
public class DataRedisTestIntegrationTests {

    @Container
    @MapPortToProperty("spring.redis.port")
    public static RedisContainer redis = new RedisContainer();

A few thoughts:

  • The examples above are JUnit 5. We'd need to consider JUnit 4 as well
  • The lifecycle ordering will need to be right so that the container's port is available before the context refreshes
  • Setting the port will affect context caching

/cc @bsideup

Comment From: bsideup

@wilkinsona Take a look at https://github.com/Playtika/testcontainers-spring-boot

Also, there are a few things to consider covering:

  • it is very important to also map the host, not just port. It is "localhost" only in ~60% cases
  • some containers provide convenient methods like KafkaContainer#getBootstrapServers, perhaps we can use a SpEL expression or something

BTW I have a bit crazy idea: What if Spring Boot implements something like:

@Testcontainers
@DataRedisTest
public class DataRedisTestIntegrationTests {

    @Container
    public static RedisContainer redis = new RedisContainer();

    @PropertyContributor
    static Callable<PropertySource> propertySource = () -> {
        return TestPropertyValues.of(
            "spring.redis.host=" + redis.getContainerIpAddress(),
            "spring.redis.port=" + redis.getMappedPort()
        );
    }

This way it can source properties from anywhere, including Testcontainers. WDYT?

Comment From: antkorwin

Hi, I think I solved this in my library (here: https://github.com/jupiter-tools/spring-test-redis), maybe it can help you:

@SpringBootTest
@RedisTestContainer
class RedisTestContainerTest {

    @Autowired
    private RedisTemplate redisTemplate;

    @Test
    void readWriteValueByRedisTemplate() {
        String key = "test";
        String value = "sabracadabra";
        // Act
        redisTemplate.opsForValue().set(key, value);
        // Assert
        assertThat(redisTemplate.opsForValue().get(key)).isEqualTo(value);
    }
}

this code run Redis test container and bind it to default spring boot properties (spring.redis.host / spring.redis.port)

I use my own ContextCustomizer, it provides an ability to start multiple containers and avoids problems with context caching in tests. Also, it provides independence on the test engine (you can use JUnit4 and JUnit5)

Example of executing multiple containers in the one test case:

@SpringBootTest
@RedisTestContainer 
@RedisTestContainer(hostTargetProperty = "second.redis.host", portTargetProperty = "second.redis.port")
@RedisTestContainer(hostTargetProperty = "third.redis.host", portTargetProperty = "third.redis.port") 
class MultipleContainerInOneTest {
    ...
}

the second container set host/port value in the specified properties, after start application context.

Maybe we can use it as part of the spring boot? I am ready to make a PR here.

Comment From: wilkinsona

Thanks for the offer, @antkorwin, but that's not quite what we're looking for here. The use of Redis was just an example and my goal is to provide something that works for any container that's being managed by Testcontainers. That would be something along the lines of the general purpose annotation in my opening description or the property contributor imagined by @bsideup.

Comment From: nosan

I have a slightly different approach. Please take a look https://github.com/nosan/spring-boot/commits/map-to-property This branch introduces @MapToProperty annotation. This annotation can be applied only to static fields.

Here is a quick example of how this annotation can be used:



class DataRedisTests {


// (2) Suppliers    
    @Container
    static final RedisContainer redis = new RedisContainer();
    @MapToProperty("spring.redis.host")
    static final Supplier<String> host  = redis::getContainerIpAddress;
    @MapToProperty("spring.redis.port")
    static final Supplier<Integer> REDIS_PORT_SUPPLIER = redis::getFirstMappedPort;

// (3) Constants    
    @MapToProperty( "spring.redis.port")
    static final int port = 6379;
// (4) Map
    @MapToProperty("spring.redis")
    static final Supplier<Map<String, Object>> properties = () -> {
        Map<String, Object> source = new LinkedHashMap<>();
        source.put("host", redis.getContainerIpAddress());
        source.put("port", redis.getFirstMappedPort());
        return source;
    };
}

Please let me know what you think.

Comment From: philwebb

The low level property mapping support is quite nice, but I can't help feeling that we should be able to apply these conventions automatically. I wonder if we can provide some pre-supplied mapping conventions that detect well known containers and map them to well known Spring Boot properties.

In other words, can we make it so that this lone will get you spring.redis.host and spring.redis.port properties:

@Testcontainers
@DataRedisTest
public class DataRedisTestIntegrationTests {

    @Container
    public static RedisContainer redis = new RedisContainer();

Comment From: wilkinsona

That feels a little bit too magic to me.

Comment From: bsideup

@philwebb as much as I like the idea of having a native support for Testcontainers in Spring Boot Test, I agree with Andy that it is too magical, plus, since only a sub-set of containers will be supported, it can be confusing when somebody will declare a container (especially the generic one) an expect Spring Boot to do the magic without realizing that it is on per-type basis. PropertyContributor looks like a good middle ground here.

@nosan

@MapToProperty("spring.redis") static final Supplier<Map<String, Object>> properties = () -> {

While I understand why you want to do this, most probably you will have more than one container, and the syntactic overhead of defining a few property contributors will overcome the benefit

Comment From: nosan

@bsideup

       @MapToProperty
    static final Supplier<Map<String, Object>> properties = () -> {
        Map<String, Object> source = new LinkedHashMap<>();
        source.put("spring.redis.port", redis.getFirstMappedPort());
        source.put("spring.redis.host", redis.getContainerIpAddress());
        source.put("spring.data.neo4j.uri", neo4j.getBoltUrl());
        return source;
    };

value attribute can be omitted for a Map. From my perspective with @PropertyContributor a bit difficult to define a simple property.

Comment From: nosan

@philwebb It would be nice to support not only TestContainers. I have a project embedded-cassandra and it has CassandraRule class which can be used in conjunction with @ClassRule to start and stop Cassandra. So with @PropertyContributor or @MapToProperty I would be able to do:



    @ClassRule
    public static final CassandraRule cassandra = new CassandraRule();

    @MapToProperty("spring.data.cassandra")
    public static final Supplier<Map<String, Object>> cassandraProperties = () -> {
        Map<String, Object> properties = new LinkedHashMap<>();
        Settings settings = cassandra.getSettings();
        properties.put("port", settings.getPort());
        properties.put("contact-points", settings.getAddress().getHostAddress());
        return properties;
    };

Comment From: nosan

I've updated my approach a bit in my branch: https://github.com/nosan/spring-boot/commits/map-to-property

Here is a list of types that can be used with @MapToProperty:

//spring=boot
@MapToProperty("spring")
String name = "boot";

//spring[0]=boot, spring[1]=framework
@MapToProperty("spring")
Collection<String> collection =Arrays.asList("boot","framework");

//spring[0]=boot, spring[1]=framework
@MapToProperty("spring")
String[]array={"boot","framework"};

//spring=boot
@MapToProperty("spring")
Callable<String> callable=()->"boot";

//spring-boot.version=2.0.0
@MapToProperty("spring-boot")
Map<String, Object> prefixMap=Collections.singletonMap("version","2.0.0");

//spring-framework.version=5.0.0
@MapToProperty
Map<String, Object> map=Collections.singletonMap("spring-framework.version","5.0.0");

//spring=boot
@MapToProperty("spring")
Optional<String> optional=Optional.of("boot");

//spring=boot
@MapToProperty("spring")
Supplier<String> supplier=()->"boot";

//spring-boot.version=2.0.0
@MapToProperty("spring-boot")
TestPropertyValues prefixValues=TestPropertyValues.of("version=2.0.0");

//spring-framework.version=5.0.0
@MapToProperty
TestPropertyValues values=TestPropertyValues.of("spring-framework.version=5.0.0");

//spring.redis.host=redis.getContainerIpAddress()
//spring.redis.port=redis.getMappedPort()
@MapToRedisProperties
 Supplier<TestPropertyValues> propertySource = () -> TestPropertyValues.of(
        "host=" + redis.getContainerIpAddress(),
        "port=" + redis.getMappedPort());

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@MapToProperty("spring.redis")
@interface MapToRedisProperties {
}

Callable, Supplier, Optional can be used with any of them. e.g.

@MapToProperty
Supplier<TestPropertyValues> values=()->TestPropertyValues.of("spring-framework.version=5.0.0");

Callable,Supplier and Optional will affect context caching.

Please let me know what you think.

Comment From: antkorwin

@nosan
I think this looks like something which I proposed earlier in my PR #17030 , at least the implementation based on a similar way.

@wilkinsona if you select this approach I will be glad to return working on this implementation. Thanks!

Comment From: nosan

@antkorwin - My approach is based on static final fields with various types. - ContextFactoryCustomizer cannot process properties because we have to be sure that the container's port is available before the context refreshes.

Consider the example:

//if we change the order of the annotations then this 
//test will fail as ContextFactoryCustomizer will be invoked before Container
@Testcontainers
@DataNeo4jTest
class DataNeo4jTestIntegrationTests {

    @Container
    static final Neo4jContainer<?> neo4j = new Neo4jContainer<>();

    @DynamicTestProperty
    private static TestPropertyValues setContainerProperties() {
        return TestPropertyValues.of("spring.data.neo4j.uri=" + neo4j.getBoltUrl);
    }

    @Test
    void name() {

    }
}

That's why I've done everything in ContextCustomizer instead of ContextCustomizerFactory. The limitation of this approach is too difficult to implement the equals and hashCode for ContextCustomizer.

There are two tests to check the lifecycle: - JUnit4 TestRule - JUnit5 Extension

Comment From: philwebb

We discussed this again today briefly and came to the conclusion that we need more time to consider all the implications. Unfortunately, we're all pretty stacked up with Spring Boot 2.2 and Spring Framework 5.2 issues so it's going to have to wait until those releases are done.

We also want to discuss with the Framework team to see if such a feature would make more sense in spring-test. Since there's already a @TestPropertySource annotation, it might be nice if it could be used on a static method (similar to @bsideup's suggestion):

@Testcontainers
@DataRedisTest
public static MyTest {

    @Container
    public static RedisContainer redis = new RedisContainer();

    @TestPropertySource
    static PropertySource propertySource() {
        return TestPropertyValues.of(
            "spring.redis.host=" + redis.getContainerIpAddress(),
            "spring.redis.port=" + redis.getMappedPort()
        );
    }

}

We'd of course need to check that the @Container is setup before the propertySource() method is invoked.

We'll try to talk about these ideas at Spring One. There won't be any decisions until then I'm afraid.

Comment From: antkorwin

@nosan, annotation ordering it's a good point,

I fixed a problem with the order of junit5 extensions, but we aren't able to evaluate value of dynamic properties before run context customizer, this can lead to reset a test context cache even if the test cases used the same properties in the dynamic property provider(property contributor) method.

For example, if we use the dynamic property in the parent abstract class:

public abstract class ParentTest {

    @DynamicTestProperty
    private static TestPropertyValues parentProperties(){
        return TestPropertyValues.of("key=123");
    }
}

and extend this class in multiple tests:

@SpringBootTest
class FirstChildTest extends ParentTest {

    @Value("${key}")
    private String key;

    @Test
    void dynamicPropertyFromSuperClass() {
        assertThat(key).isEqualTo("123");
    }
}

@SpringBootTest
class SecondChildTest extends ParentTest {

    @Value("${key}")
    private String key;

    @Test
    void dynamicPropertyFromSuperClass() {
        assertThat(key).isEqualTo("123");
    }
}

to avoid running a new application context, in this case, I check the equals of method references of dynamic property provider in DynamicTestPropertyContextCustomizer, and context caching works fine.

However in some cases, when a property has the same values but defined by different methods, this will bring to creating a new context in the cache, maybe it will be a limitation of this feature.

I updated my variant of implementation here: https://github.com/spring-projects/spring-boot/compare/master...antkorwin:add-dynamic-test-property

By the way, I can't use the TestPropertySource annotation to define a dynamic property because this annotation has a target level TYPE. This requires making changes in the Spring Framework.

I hope it will be useful..

Comment From: wilkinsona

Following a chat with @sbrannen, I've opened https://github.com/spring-projects/spring-framework/issues/24540 to track this on the Framework side.

Comment From: philwebb

The framework issue has now been fixed and you can define dynamic properties as follows:

@DynamicPropertySource
static void containerProperties(DynamicPropertyRegistry registry) {
    registry.add("test.container.ip", container::getIpAddress);
    registry.add("test.container.port", container::getPort);
}

We'll use this issue to convert our own tests and add some documentation.

Comment From: wilkinsona

I'm not sure how much of a documentation update we need in Boot. The new functionality is now described in Framework's documentation. Usage in Spring Boot would be identical other than replacing @SpringJUnitConfig(/* ... */) with, most likely, @SpringBootTest.

I think we should update our tests to use the new functionality. We can do that in 2.2.x.

Comment From: wilkinsona

Closing in favour of #20676.