繁体   English   中英

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

[英]How to reuse Testcontainers between multiple SpringBootTests?

我正在使用带有 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");

但是现在我有 14 个这样的测试,每次运行测试时都会启动一个新的 Postgres 实例。 是否可以在所有测试中重复使用同一个实例? Singleton 模式没有帮助,因为每个测试都会启动一个新的应用程序。

我还在.testcontainers.properties.withReuse(true)中尝试过testcontainers.reuse.enable=true ,但这没有帮助。

如果您想拥有可重复使用的容器,则不能使用 JUnit Jupiter 注释@Container 此注释确保在每次测试后停止容器。

您需要的是 singleton 容器方法,并使用例如@BeforeAll来启动您的容器。 即使您随后在多个测试中使用了 .start(),如果您在容器定义中同时使用.start() .withReuse(true)和您家中的以下.testcontainers.properties文件选择可重用性,Testcontainers 也不会启动新容器目录:

testcontainers.reuse.enable=true

一个简单的示例可能如下所示:

@SpringBootTest
public class SomeIT {

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

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

  @Test
  public void test() {

  }

}

和另一个集成测试:

@SpringBootTest
public class SecondIT {

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

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

  @Test
  public void secondTest() {

  }

}

目前有一个PR 增加了关于这个的文档

我整理了一篇博文,详细解释了如何使用 Testcontainers 重用容器

If you decide go forward with the singleton pattern , mind the warning in " Database containers launched via JDBC URL scheme ". 我花了几个小时才注意到,即使我使用的是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 .

接受的答案很好,但问题是您仍然必须为每个集成测试重复配置(创建、启动等)。 最好使用更少的代码行进行更简单的配置。 我认为更干净的版本会使用 JUnit 5 个扩展。

这就是我解决问题的方法。 下面的示例使用 MariaDB 容器,但该概念适用于所有容器。

  1. 创建包含 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. 创建一个扩展 class 来启动容器并设置DataSource属性。 并在需要时运行迁移:
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. 添加@ExtendWith到你的测试 class
@SpringBootTest
@ExtendWith(MariaDBSetupExtension.class)
class ApplicationIntegrationTests {

    @Test
    void someTest() {
    }

}

另一个测试

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

    @Test
    void anotherTest() {
    }

}

使用 singleton 容器或可重用容器是可能的解决方案,但因为它们不是 scope 容器的生命周期与应用程序上下文的生命周期都不太理想。

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

在测试中使用:

@Slf4j
@SpringBootTest
@EnabledPostgresTestContainer
class DemoApplicationTest {

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

}

然后在META-INF/spring.factories中启用注解:

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

可以实现为:

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

    }

}

我不确定@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.

暂无
暂无

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

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