简体   繁体   English

如何在多个 SpringBootTest 之间复用 Testcontainer?

[英]How to reuse Testcontainers between multiple SpringBootTests?

I'm using TestContainers with Spring Boot to run unit tests for repositories like this:我正在使用带有 Spring Boot 的 TestContainers 来为这样的存储库运行单元测试:

@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.但是现在我有 14 个这样的测试,每次运行测试时都会启动一个新的 Postgres 实例。 Is it possible to reuse the same instance across all tests?是否可以在所有测试中重复使用同一个实例? The Singleton pattern doesn't help since every test starts a new application. Singleton 模式没有帮助,因为每个测试都会启动一个新的应用程序。

I've also tried testcontainers.reuse.enable=true in .testcontainers.properties and .withReuse(true) , but that didn't help.我还在.testcontainers.properties.withReuse(true)中尝试过testcontainers.reuse.enable=true ,但这没有帮助。

You can't use the JUnit Jupiter annotation @Container if you want to have reusable containers.如果您想拥有可重复使用的容器,则不能使用 JUnit Jupiter 注释@Container 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.您需要的是 singleton 容器方法,并使用例如@BeforeAll来启动您的容器。 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:即使您随后在多个测试中使用了 .start(),如果您在容器定义中同时使用.start() .withReuse(true)和您家中的以下.testcontainers.properties文件选择可重用性,Testcontainers 也不会启动新容器目录:

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目前有一个PR 增加了关于这个的文档

I've put together a blog post explaining how to reuse containers with Testcontainers in detail.我整理了一篇博文,详细解释了如何使用 Testcontainers 重用容器

If you decide go forward with the singleton pattern , mind the warning in " Database containers launched via JDBC URL scheme ". 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.我花了几个小时才注意到,即使我使用的是singleton 模式,总是会创建一个额外的容器,映射到不同的端口。

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 . 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.我认为更干净的版本会使用 JUnit 5 个扩展。

This is how I solved the problem.这就是我解决问题的方法。 Below sample uses MariaDB container but the concept is applicable to all.下面的示例使用 MariaDB 容器,但该概念适用于所有容器。

  1. Create the container config holding class:创建包含 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.创建一个扩展 class 来启动容器并设置DataSource属性。 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添加@ExtendWith到你的测试 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.使用 singleton 容器或可重用容器是可能的解决方案,但因为它们不是 scope 容器的生命周期与应用程序上下文的生命周期都不太理想。

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 .但是,可以使用ContextCustomizerFactory将容器 scope 到应用程序上下文生命周期, 我已经在博客文章中对此进行了更详细的介绍

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 :然后在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.我不确定@Testcontainers是如何工作的,但我怀疑它可能会根据 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. 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.

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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