如何将 Testcontainers 与 @DataJpaTest 结合使用以避免代码重复?

Posted

技术标签:

【中文标题】如何将 Testcontainers 与 @DataJpaTest 结合使用以避免代码重复?【英文标题】:How to combine Testcontainers with @DataJpaTest avoiding code duplication? 【发布时间】:2021-10-06 15:54:08 【问题描述】:

我想通过 JUnit 5 将 Testcontainers 与 @DataJpaTest(和 @SpringBootTest)一起使用。我的基本设置使用 @Testcontainers@Container 注释,如下所示:

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

import static org.assertj.core.api.Assertions.assertThat;

@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Testcontainers
public class AtleteRepositoryTest 
    @Container
    private static final PostgreSQLContainer<?> CONTAINER = new PostgreSQLContainer<>("postgres:11");

    @DynamicPropertySource
    static void registerProperties(DynamicPropertyRegistry registry) 
        registry.add("spring.datasource.url", CONTAINER::getJdbcUrl);
        registry.add("spring.datasource.username", CONTAINER::getUsername);
        registry.add("spring.datasource.password", CONTAINER::getPassword);
    

    @Autowired
    private AtleteRepository repository;

    @Test
    void testSave() 
        repository.save(new Atlete("Wout Van Aert", 0, 1, 0));

        assertThat(repository.count()).isEqualTo(1);
    

完整的示例代码参见https://github.com/wimdeblauwe/blog-example-code/tree/feature/testcontainers-datajpatest/testcontainers-datajpatest(AtleteRepositoryTest、TeamRepositoryTest 和TestcontainersDatajpatestApplicationTests)。

为了避免重复声明PostgreSQL容器和动态属性,我尝试了以下方法:

JUnit 5 扩展

Baeldung 有一个关于如何use a JUnit 5 extension to avoid the duplication 的博客。

import org.junit.jupiter.api.extension.AfterAllCallback;
import org.junit.jupiter.api.extension.BeforeAllCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.testcontainers.containers.PostgreSQLContainer;

public class PostgreSQLExtension implements BeforeAllCallback, AfterAllCallback 

    private PostgreSQLContainer<?> postgres;

    @Override
    public void beforeAll(ExtensionContext context) 
        postgres = new PostgreSQLContainer<>("postgres:11");

        postgres.start();
        System.setProperty("spring.datasource.url", postgres.getJdbcUrl());
        System.setProperty("spring.datasource.username", postgres.getUsername());
        System.setProperty("spring.datasource.password", postgres.getPassword());
    

    @Override
    public void afterAll(ExtensionContext context) 
        postgres.stop();
    

如果您只有 1 个测试,它可以工作,但如果您同时运行多个测试(使用 IntelliJ 或 Maven)。在这种情况下,其中一项测试将失败,因为无法与数据库建立连接。 另请注意,此扩展使用DynamicPropertyRegistry,而是使用普通环境变量。 代码见feature/testcontainers-datajpatest_baeldung-extension 分支。

使用通用超类

在feature/testcontainers-datajpatest_database-base-test 分支上,我尝试使用一个通用的超类:


import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.PostgreSQLContainer;

public class DatabaseBaseTest 
    private static final PostgreSQLContainer<?> CONTAINER = new PostgreSQLContainer<>("postgres:11");

    @BeforeAll
    static void start() 
        CONTAINER.start();
    

    @AfterAll
    static void stop() 
        CONTAINER.stop();
    

    @DynamicPropertySource
    static void registerProperties(DynamicPropertyRegistry registry) 
        registry.add("spring.datasource.url", () -> 
            String jdbcUrl = CONTAINER.getJdbcUrl();
            System.out.println("jdbcUrl = " + jdbcUrl);
            return jdbcUrl;
        );
        registry.add("spring.datasource.username", CONTAINER::getUsername);
        registry.add("spring.datasource.password", CONTAINER::getPassword);
    

不幸的是,这也不起作用。我在日志记录中注意到 @DynamicPropertySource 注释方法只被调用一次,而不是每次测试,这导致我尝试选项 3:

子类中带有@DynamicPropertySource 的公共超类

当使用普通超类,但在每个子类中添加@DynamicPropertySource方法,它又可以工作了。

此类子类的示例代码:

@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public class AtleteRepositoryTest extends DatabaseBaseTest 

    @DynamicPropertySource
    static void registerProperties(DynamicPropertyRegistry registry) 
        registry.add("spring.datasource.url", () -> 
            String jdbcUrl = CONTAINER.getJdbcUrl();
            System.out.println("jdbcUrl = " + jdbcUrl);
            return jdbcUrl;
        );
        registry.add("spring.datasource.username", CONTAINER::getUsername);
        registry.add("spring.datasource.password", CONTAINER::getPassword);
    

    @Autowired
    private AtleteRepository repository;

    @Test
    void testSave() 
        repository.save(new Atlete("Wout Van Aert", 0, 1, 0));

        assertThat(repository.count()).isEqualTo(1);
    

查看分支feature/testcontainers-datajpatest_database-base-test_subclasses 了解该版本。

所以虽然它有效,但每个测试类中仍然有很多重复。

还有其他方法可以避免重复吗?

【问题讨论】:

您是否需要在测试中访问PostgreSQLContainer 实例?使用 Spring Boot,只需将 spring.datasource.url 设置为 jdbc:tc:postgresql:9.6.8:///databasename 就足够了,@DataJpaTest@SpringBootTest 将获取正确的数据源。 jdbc url jdbc:tc... 表示这是一个Testcontainr,会自动启动正确的容器。这里是官方文档testcontainers.org/modules/databases/jdbc 我知道特殊的 JDBC URL 语法,但我想要一个更通用的解决方案,以便它也可以用于其他类型的容器。 【参考方案1】:

为了避免 Testcontainers 代码重复,我通常遵循 2 种方法:

    ApplicationContextInitializer@ContextConfiguration 一起使用
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.test.util.TestPropertyValues;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.testcontainers.containers.PostgreSQLContainer;

@Slf4j
public class PostgreSQLContainerInitializer
        implements ApplicationContextInitializer<ConfigurableApplicationContext> 

    private static PostgreSQLContainer sqlContainer = new PostgreSQLContainer("postgres:10.7");

    static 
        
        sqlContainer.start();
    

    public void initialize (ConfigurableApplicationContext configurableApplicationContext)
        TestPropertyValues.of(
                "spring.datasource.url=" + sqlContainer.getJdbcUrl(),
                "spring.datasource.username=" + sqlContainer.getUsername(),
                "spring.datasource.password=" + sqlContainer.getPassword()
        ).applyTo(configurableApplicationContext.getEnvironment());
    


import com.sivalabs.myservice.common.PostgreSQLContainerInitializer;
import com.sivalabs.myservice.entities.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.test.context.ContextConfiguration;
import javax.persistence.EntityManager;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;

@DataJpaTest
@AutoConfigureTestDatabase(replace= AutoConfigureTestDatabase.Replace.NONE)
@ContextConfiguration(initializers = PostgreSQLContainerInitializer.class)
class UserRepositoryTest 

    @Autowired
    EntityManager entityManager;

    @Autowired
    private UserRepository userRepository;

    @Test
    void shouldReturnUserGivenValidCredentials() 
        User user = new User(null, "test@gmail.com", "test", "Test");
        entityManager.persist(user);
        
        Optional<User> userOptional = userRepository.login("test@gmail.com", "test");
        
        assertThat(userOptional).isNotEmpty();
    

    Java 8+ 接口中使用 @DynamicPropertySource
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

@Testcontainers
public interface PostgreSQLContainerInitializer 

    @Container
    PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:12.3");

    @DynamicPropertySource
    static void registerPgProperties(DynamicPropertyRegistry registry) 
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
    

@DataJpaTest
@AutoConfigureTestDatabase(replace= AutoConfigureTestDatabase.Replace.NONE)
class UserRepositoryTest implements PostgreSQLContainerInitializer 

    ....
    ....

通过这些方法,我们不必重复 PostgreSQLContainer 声明和 Spring 属性设置。

是否使用 PostgreSQLContainer 作为 static 字段取决于您是要为每个测试启动一个新容器还是每个测试类启动一个容器。 p>

PS: 我避免使用通用基类方法,因为有时一个测试只需要 1 个容器,而另一个测试需要多个容器。如果我们将所有容器添加到公共基类中,那么对于每个测试/类,所有这些容器都将启动,而不管它们的使用情况如何,这会使测试变得非常慢。

【讨论】:

在测试了这两种方法后,我发现这两种方法都适用于单个测试,但如果您有很多测试,只有方法 1 可以正常工作。我现在为我的测试套件使用方法 1。谢谢!

以上是关于如何将 Testcontainers 与 @DataJpaTest 结合使用以避免代码重复?的主要内容,如果未能解决你的问题,请参考以下文章

尝试将 Testcontainers 规则与抽象类中的网络一起使用时出现“没有这样的网络”错误

使用 testcontainers 测试 kafka 和 spark

如何使用 Testcontainers 将可执行文件复制到 Docker 容器

如何获取使用 Testcontainers 执行的命令退出代码?

TestContainers Kafka 清理

如何使用 Testcontainers 发送信号?