spring-projects / spring-boot

Spring Boot
https://spring.io/projects/spring-boot
Apache License 2.0
73.89k stars 40.38k forks source link

Allow to use Testcontainers with @AutoConfigureTestDatabase #19038

Open snicoll opened 4 years ago

snicoll commented 4 years ago

Follow-up of this limitation reported on SO, it would be nice if users were able to configure the URL that @AutoConfigureTestDatabase uses. Right now if they want a specific dialect or any other extra parameter, they have to switch off our support to configure things manually.

snicoll commented 4 years ago

We've spent some time discussing this feature and we're willing to investigate a support of testcontainers rather which would provide all the necessary flexibility.

snicoll commented 4 years ago

Now that we have r2dbc support, we should also take that use case into account so that the ConectionFactory points to a database replaced via this mechanism.

wilkinsona commented 3 years ago

I've been experimenting a little bit in this area. I have a test class like this:

@JdbcTest
@AutoConfigureTestDatabase(replace = Replace.NONE)
@Testcontainers(disabledWithoutDocker = true)
@Import(SomeRepository.class)
@Sql(scripts = "data.sql")
class SomeRepositoryTests {

    @Container
    private static final PostgreSQLContainer<?> postgresqlContainer = new PostgreSQLContainer<>("postgres:13.2")
            .withDatabaseName("example");

    @Autowired
    private SomeRepository someRepository;

        // @Test methods

I've then got a context customizer factory and a context customizer that turns each JdbcDatabaseContainer field into a DataSource bean:

/**
 * A {@link ContextCustomizerFactory} that returns a
 * {@link JdbcDatabaseContainerContextCustomizer} when {@link JdbcDatabaseContainer} is on
 * the classpath and the test class has one or more static fields of that type.
 *
 * @author Andy Wilkinson
 */
class JdbcDatabaseContainerContextCustomizerFactory implements ContextCustomizerFactory {

    @Override
    public ContextCustomizer createContextCustomizer(Class<?> testClass,
            List<ContextConfigurationAttributes> configAttributes) {
        if (ClassUtils.isPresent("org.testcontainers.containers.JdbcDatabaseContainer", testClass.getClassLoader())) {
            Set<Field> jdbcDatabaseContainerFields = new HashSet<>();
            ReflectionUtils.doWithFields(testClass, (field) -> {
                if (Modifier.isStatic(field.getModifiers())
                        && JdbcDatabaseContainer.class.isAssignableFrom(field.getType())) {
                    ReflectionUtils.makeAccessible(field);
                    jdbcDatabaseContainerFields.add(field);
                }
            });
            if (!jdbcDatabaseContainerFields.isEmpty()) {
                return new JdbcDatabaseContainerContextCustomizer(jdbcDatabaseContainerFields);
            }
        }
        return null;
    }

}
/**
 * {@link ContextCustomizer} that registers a {@link DataSource} bean for each
 * {@link JdbcDatabaseContainer} field in the test class.
 *
 * @author Andy Wilkinson
 */
class JdbcDatabaseContainerContextCustomizer implements ContextCustomizer {

    private final Set<Field> jdbcDatabaseContainerFields;

    JdbcDatabaseContainerContextCustomizer(Set<Field> jdbcDatabaseContainerFields) {
        this.jdbcDatabaseContainerFields = jdbcDatabaseContainerFields;
    }

    @Override
    public void customizeContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) {
        for (Field field : this.jdbcDatabaseContainerFields) {
            JdbcDatabaseContainer<?> container = (JdbcDatabaseContainer<?>) ReflectionUtils.getField(field, null);
            BeanDefinition definition = new RootBeanDefinition(DataSource.class,
                    () -> DataSourceBuilder.create(context.getClassLoader()).url(container.getJdbcUrl())
                            .username(container.getUsername()).password(container.getPassword()).build());
            ((BeanDefinitionRegistry) context.getBeanFactory()).registerBeanDefinition(field.getName(), definition);
        }
    }

}

I think this isn't too far off. I'd prefer it if @AutoConfigureTestDatabase(replace = Replace.NONE) wasn't necessary. If this context customization was done in Boot itself, we'd also need some way of opting in as it'd be too much for it to always happen. Perhaps a @TestDatabase annotation on the test's postgresqlContainer field?

The above isn't hugely Testcontainers-specific and I wonder if there may be some mileage in some general support for adding beans to the context or setting properties in the environment that are derived from static fields in a test class. It could be useful for https://github.com/spring-projects/spring-boot/issues/27151 as well, for example, with a mechanism that allows the JdbcDatabaseContainer to DataSource bean or org.neo4j.harness.Neo4j to spring.neo4j.* properties capability to be contributed.