如何使用多个 MariaDB 容器测试 Spring Boot 应用程序?

问题描述 投票:0回答:1

我正在开发一个使用全局数据库和本地数据库(一个数据库,两个模式)的 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 容器成功测试应用程序?我应该修改现有配置,还是有更合适的方法来处理数据库连接以进行测试?

java spring-boot mariadb testcontainers
1个回答
0
投票

虽然我解决了问题,但问题很可能是由于配置不可用而未正确创建测试数据库引起的。这是修改后的代码和一些解释:

我使用了现有的数据库配置,我设法在 IntegrationTestBase 类中正确使用该配置,如下所示:

@Import({FlywayConfig.class, LocalDbConfig.class, GlobalDbConfig.class})
  • @Import注释确保IntegrationTestBase类可以正确使用所需的配置

我编写了一个单独的 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);
        }
    }
© www.soinside.com 2019 - 2024. All rights reserved.