简体   繁体   中英

How to reuse Testcontainers between multiple SpringBootTests?

I'm using TestContainers with Spring Boot to run unit tests for repositories like this:

@Testcontainers
@ExtendWith(SpringExtension.class)
@ActiveProfiles("itest")
@SpringBootTest(classes = RouteTestingCheapRouteDetector.class)
@ContextConfiguration(initializers = AlwaysFailingRouteRepositoryShould.Initializer.class)
@TestExecutionListeners(listeners = DependencyInjectionTestExecutionListener.class)
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Tag("docker")
@Tag("database")
class AlwaysFailingRouteRepositoryShould {

  @SuppressWarnings("rawtypes")
  @Container
  private static final PostgreSQLContainer database =
      new PostgreSQLContainer("postgres:9.6")
          .withDatabaseName("database")
          .withUsername("postgres")
          .withPassword("postgres");

But now I have 14 of these tests and every time a test is run a new instance of Postgres is spun up. Is it possible to reuse the same instance across all tests? The Singleton pattern doesn't help since every test starts a new application.

I've also tried testcontainers.reuse.enable=true in .testcontainers.properties and .withReuse(true) , but that didn't help.

You can't use the JUnit Jupiter annotation @Container if you want to have reusable containers. This annotation ensures to stop the container after each test .

What you need is the singleton container approach, and use eg @BeforeAll to start your containers. Even though you then have .start() in multiple tests, Testcontainers won't start a new container if you opted-in for reusability using both .withReuse(true) on your container definition AND the following .testcontainers.properties file in your home directory:

testcontainers.reuse.enable=true

A simple example might look like the following:

@SpringBootTest
public class SomeIT {

  public static GenericContainer postgreSQLContainer = new PostgreSQLContainer().
    withReuse(true);

  @BeforeAll
  public static void beforeAll() {
    postgreSQLContainer.start();
  }

  @Test
  public void test() {

  }

}

and another integration test:

@SpringBootTest
public class SecondIT {

  public static GenericContainer postgreSQLContainer = new PostgreSQLContainer().
    withReuse(true);

  @BeforeAll
  public static void beforeAll() {
    postgreSQLContainer.start();
  }

  @Test
  public void secondTest() {

  }

}

There is currently aPR that adds documentation about this

I've put together a blog post explaining how to reuse containers with Testcontainers in detail.

If you decide go forward with the singleton pattern , mind the warning in " Database containers launched via JDBC URL scheme ". I took hours till I note that, even though I was using the singleton pattern , an additional container was always being created mapped on a different port.

In summary, do not use the test containers JDBC (host-less) URIs, such as jdbc:tc:postgresql:<image-tag>:///<databasename> , if you need use the singleton pattern .

Accepted answer is great but the problem is you still have to repeat the configurations(creating, starting and etc.) for each integration tests. It would be better to have simpler configuration with fewer lines of code. I think cleaner version would be using JUnit 5 extensions.

This is how I solved the problem. Below sample uses MariaDB container but the concept is applicable to all.

  1. Create the container config holding class:
public class AppMariaDBContainer extends MariaDBContainer<AppMariaDBContainer> {

    private static final String IMAGE_VERSION = "mariadb:10.5";
    private static final String DATABASE_NAME = "my-db";
    private static final String USERNAME = "user";
    private static final String PASSWORD = "strong-password";

    public static AppMariaDBContainer container = new AppMariaDBContainer()
            .withDatabaseName(DATABASE_NAME)
            .withUsername(USERNAME)
            .withPassword(PASSWORD);

    public AppMariaDBContainer() {
        super(IMAGE_VERSION);
    }

}
  1. Create an extension class that starts the container and sets the DataSource properties. And run migrations if needed:
public class DatabaseSetupExtension implements BeforeAllCallback {

    @Override
    public void beforeAll(ExtensionContext context) {
        AppMariaDBContainer.container.start();
        updateDataSourceProps(AppMariaDBContainer.container);
        //migration logic here (if needed)
    }

    private void updateDataSourceProps(AppMariaDBContainer container) {
        System.setProperty("spring.datasource.url", container.getJdbcUrl());
        System.setProperty("spring.datasource.username", container.getUsername());
        System.setProperty("spring.datasource.password", container.getPassword());
    }

}
  1. Add @ExtendWith to your test class
@SpringBootTest
@ExtendWith(MariaDBSetupExtension.class)
class ApplicationIntegrationTests {

    @Test
    void someTest() {
    }

}

Another test

@SpringBootTest
@ExtendWith(MariaDBSetupExtension.class)
class AnotherIntegrationTests {

    @Test
    void anotherTest() {
    }

}

Using either singleton containers or reusable containers are possible solutions but because they don't scope the life-cycle of the container to that of the application context both are less then ideal.

It is however possible to scope the container to the application contexts lifecycle by using a ContextCustomizerFactory and I've written about this in more detail in a blog post .

In a test use:

@Slf4j
@SpringBootTest
@EnabledPostgresTestContainer
class DemoApplicationTest {

    @Test
    void contextLoads() {
        log.info("Hello world");
    }

}

Then enable the annotation in META-INF/spring.factories :

org.springframework.test.context.ContextCustomizerFactory=\
  com.logarithmicwhale.demo.EnablePostgresTestContainerContextCustomizerFactory

Which can be implemented as:

public class EnablePostgresTestContainerContextCustomizerFactory implements ContextCustomizerFactory {

    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Inherited
    public @interface EnabledPostgresTestContainer {
    }

    @Override
    public ContextCustomizer createContextCustomizer(Class<?> testClass,
            List<ContextConfigurationAttributes> configAttributes) {
        if (!(AnnotatedElementUtils.hasAnnotation(testClass, EnabledPostgresTestContainer.class))) {
            return null;
        }
        return new PostgresTestContainerContextCustomizer();
    }

    @EqualsAndHashCode // See ContextCustomizer java doc
    private static class PostgresTestContainerContextCustomizer implements ContextCustomizer {

        private static final DockerImageName image = DockerImageName
                .parse("postgres")
                .withTag("14.1");

        @Override
        public void customizeContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) {
            var postgresContainer = new PostgreSQLContainer<>(image);
            postgresContainer.start();
            var properties = Map.<String, Object>of(
                    "spring.datasource.url", postgresContainer.getJdbcUrl(),
                    "spring.datasource.username", postgresContainer.getUsername(),
                    "spring.datasource.password", postgresContainer.getPassword(),
                    // Prevent any in memory db from replacing the data source
                    // See @AutoConfigureTestDatabase
                    "spring.test.database.replace", "NONE"
            );
            var propertySource = new MapPropertySource("PostgresContainer Test Properties", properties);
            context.getEnvironment().getPropertySources().addFirst(propertySource);
        }

    }

}

I'm not sure how @Testcontainers works, but I suspect it might work per class.

Just make your singleton static as described in Singleton pattern and get it in every test from your signleton holder, don't define it in every test class.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM