我正在开发一个使用全局数据库和本地数据库(一个数据库,两个模式)的 Spring Boot 应用程序。这是当前与应用程序配合良好的数据库配置(我只发送本地配置,全局配置是相同的,但显然具有不同的包和名称):
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
basePackages = "com.myapp.local",
entityManagerFactoryRef = "localEntityManagerFactory",
transactionManagerRef = "localTransactionManager"
)
public class LocalDbConfig {
@Value("${spring.datasource.local.url}")
private String url;
@Value("${spring.datasource.local.username}")
private String username;
@Value("${spring.datasource.local.password}")
private String password;
@Value("${spring.datasource.local.driver-class-name}")
private String driverClassName;
@Primary
@Bean(name = "localDbDataSource")
public DataSource localDbDataSource() {
return DataSourceBuilder.create()
.url(url)
.username(username)
.password(password)
.driverClassName(driverClassName)
.build();
}
@Primary
@Bean(name = "localEntityManagerFactory")
public LocalContainerEntityManagerFactoryBean localEntityManagerFactory(EntityManagerFactoryBuilder builder, @Qualifier("localDbDataSource") DataSource localDataSource) {
Map<String, String> props = new HashMap<>();
props.put("hibernate.physical_naming_strategy", CamelCaseToUnderscoresNamingStrategy.class.getName());
return builder
.dataSource(localDataSource)
.packages("com.myapp.local")
.properties(props)
.build();
}
@Primary
@Bean(name = "localTransactionManager")
public PlatformTransactionManager localTransactionManager(@Qualifier("localEntityManagerFactory") EntityManagerFactory localEntityManagerFactory) {
return new JpaTransactionManager(localEntityManagerFactory);
}
}
我想使用两个成功启动的 MariaDB 容器来测试应用程序。但是,两个容器都会停止并出现以下错误: “等待数据库连接在 jdbc:mysql://x.x.x.x:xxxx/test 使用查询“SELECT 1”变得可用
这是我的 TestContainersInitializer:
@TestConfiguration
public class TestContainersInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
private static final Network SHARED_NETWORK = Network.newNetwork();
private static final MariaDBContainer<?> localMariaDB = new MariaDBContainer<>(DockerImageName.parse("mariadb:latest"))
.withNetwork(SHARED_NETWORK)
.withDatabaseName("local_db")
.withUsername("root")
.withPassword("test")
.withReuse(true);
private static final MariaDBContainer<?> globalMariaDB = new MariaDBContainer<>(DockerImageName.parse("mariadb:latest"))
.withNetwork(SHARED_NETWORK)
.withDatabaseName("global_db")
.withUsername("root")
.withPassword("test")
.withReuse(true);
private static final KeycloakContainer keycloak = new KeycloakContainer()
.withNetwork(SHARED_NETWORK)
.withRealmImportFile("test-realm-export.json")
.withAdminUsername("keycloakadmin")
.withAdminPassword("keycloakadmin")
.withReuse(true);
private static final KafkaContainer kafka = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:latest"))
.withNetwork(SHARED_NETWORK)
.withReuse(true);
static {
Startables.deepStart(localMariaDB, globalMariaDB, keycloak, kafka).join();
}
@Override
public void initialize(@NotNull ConfigurableApplicationContext applicationContext) {
TestPropertyValues.of(
"spring.datasource.local.url=" + localMariaDB.getJdbcUrl(),
"spring.datasource.local.username=" + localMariaDB.getUsername(),
"spring.datasource.local.password=" + localMariaDB.getPassword(),
"spring.datasource.local.driver-class-name=" + localMariaDB.getDriverClassName(),
"spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MariaDBDialect",
"spring.datasource.global.url=" + globalMariaDB.getJdbcUrl(),
"spring.datasource.global.username=" + globalMariaDB.getUsername(),
"spring.datasource.global.password=" + globalMariaDB.getPassword(),
"spring.datasource.global.driver-class-name=" + globalMariaDB.getDriverClassName(),
"spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MariaDBDialect",
"keycloak.server-url=http://localhost:" + keycloak.getFirstMappedPort(),
"spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:" + keycloak.getFirstMappedPort() + "/realms/app",
"spring.kafka.bootstrap-servers=" + kafka.getBootstrapServers()
).applyTo(applicationContext.getEnvironment());
}
}
我在我的 IntegrationTestBase 类中以这种方式使用它:
@ContextConfiguration(initializers = TestContainersInitializer.class)
我不知道这种方法还需要什么才能发挥作用(使用现有的配置和容器数据)。
我还尝试为数据库编写单独的测试配置:
@TestConfiguration
public class TestDatabaseConfiguration {
@Primary
@Bean
public DataSource localDataSource(Environment env) {
return DataSourceBuilder.create()
.url(env.getProperty("spring.datasource.local.url"))
.username(env.getProperty("spring.datasource.local.username"))
.password(env.getProperty("spring.datasource.local.password"))
.driverClassName(env.getProperty("spring.datasource.local.driver-class-name"))
.build();
}
@Bean
@Qualifier("globalDataSource")
public DataSource globalDataSource(Environment env) {
return DataSourceBuilder.create()
.url(env.getProperty("spring.datasource.global.url"))
.username(env.getProperty("spring.datasource.global.username"))
.password(env.getProperty("spring.datasource.global.password"))
.driverClassName(env.getProperty("spring.datasource.global.driver-class-name"))
.build();
}
@Primary
@Bean
public LocalContainerEntityManagerFactoryBean localEntityManagerFactory(
EntityManagerFactoryBuilder builder,
DataSource localDataSource) {
Map<String, String> properties = new HashMap<>();
properties.put("hibernate.hbm2ddl.auto", "validate");
properties.put("hibernate.dialect", "org.hibernate.dialect.MariaDBDialect");
return builder
.dataSource(localDataSource)
.packages("com.utitech.bidhubbackend.local", "com.utitech.bidhubbackend.common.fileupload")
.properties(properties)
.build();
}
@Bean
public LocalContainerEntityManagerFactoryBean globalEntityManagerFactory(
EntityManagerFactoryBuilder builder,
@Qualifier("globalDataSource") DataSource globalDataSource) {
Map<String, String> properties = new HashMap<>();
properties.put("hibernate.hbm2ddl.auto", "validate");
properties.put("hibernate.dialect", "org.hibernate.dialect.MariaDBDialect");
return builder
.dataSource(globalDataSource)
.packages("com.utitech.bidhubbackend.global", "com.utitech.bidhubbackend.common.fileupload")
.properties(properties)
.build();
}
@Primary
@Bean
public PlatformTransactionManager localTransactionManager(
@Qualifier("localEntityManagerFactory") EntityManagerFactory entityManagerFactory) {
return new JpaTransactionManager(entityManagerFactory);
}
@Bean
public PlatformTransactionManager globalTransactionManager(
@Qualifier("globalEntityManagerFactory") EntityManagerFactory entityManagerFactory) {
return new JpaTransactionManager(entityManagerFactory);
}
}
对于这种方法,在 IntegrationTestBase 中我使用了属性 properties = {"spring.main.allow-bean-definition-overriding=true"},它应该允许覆盖类似名称的 bean。但是,无论我尝试什么,我仍然面临“等待数据库”问题。
我应该采取哪些步骤来解决此问题并使用多个 MariaDB 容器成功测试应用程序?我应该修改现有配置,还是有更合适的方法来处理数据库连接以进行测试?
虽然我解决了问题,但问题很可能是由于配置不可用而未正确创建测试数据库引起的。这是修改后的代码和一些解释:
我使用了现有的数据库配置,我设法在 IntegrationTestBase 类中正确使用该配置,如下所示:
@Import({FlywayConfig.class, LocalDbConfig.class, GlobalDbConfig.class})
我编写了一个单独的 Flyway 配置,它提供了两个单独的 Flyway 以确保本地和全局模式的正确迁移:
@TestConfiguration
public class FlywayConfig {
@Bean(name = "localFlyway")
public Flyway localFlyway(@Qualifier("localDbDataSource") DataSource localDataSource) {
return Flyway.configure()
.dataSource(localDataSource)
.locations("classpath:db/migration")
.load();
}
@Bean(name = "globalFlyway")
public Flyway globalFlyway(@Qualifier("globalDbDataSource") DataSource globalDataSource) {
return Flyway.configure()
.dataSource(globalDataSource)
.locations("classpath:db/migration/test")
.load();
}
}
我最终不是在单独的文件(TestContainersInitializer)中启动容器,而是在IntegrationTestBase中启动容器。 我改变的是db容器的创建。我创建容器时没有设置用户名和密码,因为这覆盖了默认的用户和密码,这也导致了问题。
这是我的 IntegrationTestBase 类,其中包含所有重要信息:
@SpringBootTest(classes = MyApp.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
@ActiveProfiles("test")
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@TestPropertySource(locations = "classpath:application-test.properties")
@Import({FlywayConfig.class, LocalDbConfig.class, GlobalDbConfig.class})
public class IntegrationTestBase {
private static final Logger LOGGER = LoggerFactory.getLogger(IntegrationTestBase.class.getName());
@Autowired
public MockMvc mockMvc;
@Autowired
private DatabaseCleaner databaseCleaner;
@Autowired
private @Qualifier("localFlyway") Flyway localFlyway;
@Autowired
private @Qualifier("globalFlyway") Flyway globalFlyway;
@Container
public static final MariaDBContainer<?> localDbContainer = new MariaDBContainer<>("mariadb:latest")
.withUrlParam("serverTimezone", "UTC")
.withReuse(true);
@Container
public static final MariaDBContainer<?> globalDbContainer = new MariaDBContainer<>("mariadb:latest")
.withUrlParam("serverTimezone", "UTC")
.withReuse(true);
static {
Startables.deepStart(localDbContainer, globalDbContainer).join();
}
@DynamicPropertySource
static void setProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.local.url", localDbContainer::getJdbcUrl);
registry.add("spring.datasource.local.username", localDbContainer::getUsername);
registry.add("spring.datasource.local.password", localDbContainer::getPassword);
registry.add("spring.datasource.local.driver-class-name", localDbContainer::getDriverClassName);
registry.add("spring.datasource.global.url", globalDbContainer::getJdbcUrl);
registry.add("spring.datasource.global.username", globalDbContainer::getUsername);
registry.add("spring.datasource.global.password", globalDbContainer::getPassword);
registry.add("spring.datasource.global.driver-class-name", globalDbContainer::getDriverClassName);
}
@BeforeAll
public void setUp() {
LOGGER.info("Running database migrations...");
localFlyway.migrate();
globalFlyway.migrate();
}
此外,这是我的数据库清理逻辑,我将其编写为单独的组件:
@Component
public class DatabaseCleaner {
private static final Logger LOGGER = LoggerFactory.getLogger(DatabaseCleaner.class);
private final DataSource localDataSource;
private final DataSource globalDataSource;
public DatabaseCleaner(@Qualifier("localDbDataSource") DataSource localDataSource,
@Qualifier("globalDbDataSource") DataSource globalDataSource) {
this.localDataSource = localDataSource;
this.globalDataSource = globalDataSource;
}
public void cleanLocal() {
executeCleanupScript(localDataSource, "/clean_up_local.sql");
}
public void cleanGlobal() {
executeCleanupScript(globalDataSource, "/clean_up_global.sql");
}
private void executeCleanupScript(DataSource dataSource, String scriptPath) {
try (Connection connection = dataSource.getConnection()) {
Resource resource = new ClassPathResource(scriptPath);
ScriptUtils.executeSqlScript(connection, resource);
} catch (Exception e) {
LOGGER.error("Error executing cleanup script: " + scriptPath, e);
throw new RuntimeException("Database cleanup failed", e);
}
}
}
我也在IntegrationTestBase中使用它,如下:
@AfterEach
public void cleanUpEach() {
try {
databaseCleaner.cleanLocal();
databaseCleaner.cleanGlobal();
} catch (Exception e) {
LOGGER.error("Cleanup failed", e);
throw new RuntimeException("Test cleanup failed", e);
}
}