java - 如何将 Testcontainers 与 @DataJpaTest 结合使用以避免代码重复?
问题描述
我想使用 JUnit 5 将 Testcontainers 与@DataJpaTest
(and @SpringBootTest
) 一起使用。我的基本设置使用@Testcontainers
and@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);
}
}
有关完整示例代码(AtleteRepositoryTest、TeamRepositoryTest和TestcontainersDatajpatestApplicationTests) ,请参阅https://github.com/wimdeblauwe/blog-example-code/tree/feature/testcontainers-datajpatest/testcontainers-datajpatest 。
为了避免重复声明 PostgreSQL 容器和动态属性,我尝试了以下方法:
JUnit 5 扩展
Baeldung 有一篇关于如何使用 JUnit 5 扩展来避免重复的博客。
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-datajptest_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-datajptest_database-base-test_subclasses。
因此,虽然它有效,但每个测试类中仍然存在很多重复。
有没有其他选择可以避免重复?
解决方案
为了避免 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 {
....
....
}
通过这些方法,我们不必重复PostgreSQL 容器声明和 Spring 属性设置。
是否使用PostgreSQLContainer作为静态字段取决于您是要为每个测试启动一个新容器还是每个测试类启动一个容器。
PS: 我避免使用通用基类方法,因为有时一个测试只需要 1 个容器,而另一个测试需要多个容器。如果我们按照在公共基类中添加所有容器,那么对于每个测试/类,所有这些容器都将启动,而不管它们的使用情况如何,这会使测试变得非常慢。
推荐阅读
- python - 如何使用flask将字符串变量转换为html中的html?
- node.js - 使用文本索引在聚合管道中填充的 Mongo 搜索
- sql - 使用活动目录登录
- python - 如何修复分类分类中的“val_accuracy:0.0000e+00”?
- php - Laravel 内部带有另一个表格和条件
- javascript - 缺少 Javascript。1 个主页面,2 个用户控制
- python - Django - 如何发出通知?当用户注册时它会向管理员的电子邮件发送通知
- javascript - 从数组对象中获取数字的总和
- javascript - 保持监听服务Angular 8的功能
- zpl - 霍尼韦尔海豚 CT60 无法扫描 gs128 条码