如何在多个SpringBootTests之间重复使用Testcontainers?

45

我正在使用TestContainers和Spring Boot运行存储库的单元测试,例如:

@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实例。是否可以在所有测试中重用同一实例?由于每次测试都会启动一个新应用程序,单例模式并没有帮助。

我还尝试了在.testcontainers.properties.withReuse(true)中使用testcontainers.reuse.enable=true,但这并没有起到帮助作用。


你尝试过使用withReuse(true)选项吗? - P3trur0
@P3trur0:是的。而且 testcontainers.reuse.enable=true - 也没有帮助。 - Martin Schröder
尝试将容器实例放置到单独的@TestConfiguration中作为@Bean,然后在所有相关测试中导入此配置。 - Nikolai Shevchenko
@NikolaiShevchenko:尝试过了,但需要在@ContextConfiguration(initializers中使用,我无法让它在那里工作。 - Martin Schröder
7个回答

68
如果您想要可重复使用的容器,就不能使用JUnit Jupiter注释@Container。该注释确保在每个测试后停止容器after each test
您需要的是单例容器方法,例如使用@BeforeAll来启动容器。即使在多个测试中有.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容器


1
testcontainers.reuse.enable=true 的文档在哪里可以找到? - Abbadon
2
是的,有多种方法可以配置此属性。请查看 Testcontainers 文档中的 配置位置部分 - rieckpil
1
无法通过类路径设置testcontainers.reuse.enable请参见此评论 - Yaroslav Buhaiev
1
当您的测试正在运行时,请查看docker ps。您应该只看到一个容器正在使用。如果您在Linux / macOS上,则可以使用“watch -n 1 docker ps”。 - rieckpil
1
@vault 在启动容器之前,您可以通过 TestcontainersConfiguration.getInstance().updateUserConfig("testcontainers.reuse.enable", "true"); 来执行此操作。 - Frankie Drake
显示剩余8条评论

9
如果您决定采用单例模式,请注意“通过JDBC URL方案启动的数据库容器”中的警告。我花了几个小时才意识到,即使我在使用单例模式,仍然会创建一个额外的容器,映射到不同的端口。

总之,如果您需要使用单例模式,请勿使用测试容器的JDBC(无主机)URI,例如jdbc:tc:postgresql:<image-tag>:///<databasename>


9

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

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

  1. 创建容器配置持有类:
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);
    }

}

创建一个扩展类,启动容器并设置 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
@SpringBootTest
@ExtendWith(DatabaseSetupExtension.class)
class ApplicationIntegrationTests {

    @Test
    void someTest() {
    }

}

另一个测试

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

    @Test
    void anotherTest() {
    }

}

应该是@ExtendWith(DatabaseSetupExtension.class) - cstroe
由于某些原因,我的容器无法被重用:https://stackoverflow.com/questions/77144407/why-testcontainer-is-not-reused-in-spring-boot-test - undefined

4
使用单例容器或可重用容器都是可能的解决方案,但由于它们没有将容器的生命周期限定在应用程序上下文中,因此两者都不是理想的选择。
然而,可以通过使用ContextCustomizerFactory将容器的范围限定为应用程序上下文的生命周期,并且我在一篇博客文章中更详细地介绍了这个方法
在测试中使用:
@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解决方案,非常喜欢你的方法,但在测试时我发现 return new PostgresTestContainerContextCustomizer(); 会每次调用时创建一个新实例,并且这会弄脏上下文,强制spring boot重新加载它。 我理解有误吗?也许将 ContextCustomizer 缓存为静态字段/单例模式最好,这样它就不会为每个测试类创建一个新实例了? - Shebo
我猜你在示例中错过了这个@EqualsAndHashCode // See ContextCustomizer java doc。Javadoc解释了为什么需要equals和hashcode以及它们与您当前看到的行为之间的关系。 - M.P. Korstanje
在此处查看它的实际效果!https://github.com/jhipster/generator-jhipster/blob/main/generators/server/templates/src/test/java/package/config/TestContainersSpringContextCustomizerFactory.java.ejs - Tcharl
@Tcharl 非常酷! - M.P. Korstanje
https://stackoverflow.com/questions/77144407/why-testcontainer-is-not-reused-in-spring-boot-test - undefined

2
接受的答案有一个警告,即容器将在测试执行期间保留下来,这可能是不可取的,因为它会导致测试无法重现。
如果目标是在多个测试类之间只有一个PostgreSQL容器实例,并在测试阶段结束时将其丢弃,可以很容易地实现,而无需依赖可重用的testcontainers。
首先创建一个抽象类,负责设置所需的testcontainers。
public abstract class BaseIntegrationTest {

    @ServiceConnection
    protected static final PostgreSQLContainer<?> dbContainer = new PostgreSQLContainer<>(
            DockerImageName.parse("postgres")
                    .withTag("15.4"))
            .withDatabaseName("testcontainer")
            .withUsername("user")
            .withPassword("pass");
 
    static {
        dbContainer.start();
    }
}

将每个测试类实现为此抽象类的扩展
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ExtendWith(SpringExtension.class)
public class BootstrapApplicationTest extends BaseIntegrationTest {

    @Test
    @DisplayName("My test case")
    void myTest() throws Exception {

        ...
    }

}

请注意,这里没有使用@TestContainers注解,因为它告诉Spring每个测试类都创建一个容器。

0

只需按照testcontainers文档中有关单例模式的指南即可。正如他们所说 - 这是JUnit5的一个选项。 不要使用@Testcontainers注释和@Container注释。它们与JUnit4相关。 还要将testcontainers.properties文件添加到类路径中:

testcontainers.reuse.enable=true

对我来说,这个就是解决办法。


-3

我不确定 @Testcontainers 是如何工作的,但我怀疑它可能是按类别工作的。

只需按照 单例模式 中描述的方式使您的单例静态化,并从您的单例持有者中获取它,在每个测试中都不要在每个测试类中定义它。


3
由于每个测试都会启动一个新的应用程序,因此单例模式无法起到帮助作用。 - Martin Schröder

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接