代码之家  ›  专栏  ›  技术社区  ›  Wim Deblauwe

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

  •  0
  • Wim Deblauwe  · 技术社区  · 4 年前

    我想将Testcontainers用于 @DataJpaTest (及 @SpringBootTest )使用JUnit5。我使用 @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();
        }
    }
    

    如果你只有一次测试,它是有效的,但是 如果同时运行多个(使用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:

    具有 @动态属性源 在子类中

    当使用公共超类,但添加 @动态属性源 方法在每个子类中再次工作。

    此类子类的示例代码:

    @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 对于那个版本。

    因此,尽管它有效,但每个测试类中仍然存在大量重复。

    有没有其他避免重复的方法?

    0 回复  |  直到 4 年前
        1
  •  6
  •   K. Siva Prasad Reddy paulchapman    4 年前

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

    1. 使用 ApplicationContextInitializer 具有 @上下文配置
    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();
        }
    }
    
    1. 使用 @动态属性源 在里面 Java 8+接口
    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 作为一个 静止的 字段是否可用取决于您是希望为每个测试启动一个新容器,还是希望为每个测试类启动一个容器。

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