【问题标题】:How to drop in-memory h2db between Spring Integration tests?如何在 Spring Integration 测试之间删除内存中的 h2db?
【发布时间】:2017-03-05 17:57:48
【问题描述】:

我在我的 Spring Web 应用程序中使用 Liquibase。我有一堆实体,在每个实体(如用户、帐户、发票、许可证等)的集成测试中对 REST API 进行了数百次测试。我的所有集成测试在按类运行时都通过了,但在使用gradle test。测试之间很可能存在数据冲突,到目前为止,我对花时间修复清理数据不感兴趣。我更喜欢在每节课后删除数据库和上下文。我想我可以在课堂上使用@DirtiesContext,所以我通过测试对其进行了注释。

@RunWith(SpringRunner.class)
@SpringBootTest(classes = {Application.class, SecurityConfiguration.class},
    webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@DirtiesContext
public class InvoiceResourceIntTest {

我看到添加注释后,每个类都会启动 Web 应用程序上下文,但是当 Liquibase 初始化发生时,查询不会运行,因为校验和匹配。由于这是一个内存数据库,我预计数据库会与 spring 上下文一起被销毁,但它没有发生。

我还将 jpa hibernate ddl-auto 设置为 create-drop 但这没有帮助。我正在考虑的下一个选项是代替mem,将h2db 写入文件并在我的集成测试类文件的@BeforeClass 中删除该文件。我更喜欢在内存中自动删除 db 而不是在测试中管理它,但想在这里尝试作为最后的选择。感谢您的帮助。

更新:

我更新了如下测试。

@RunWith(SpringRunner.class)
@SpringBootTest(classes = {Application.class, SecurityConfiguration.class},
    webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
    properties = "spring.datasource.name=AccountResource")
@DirtiesContext
public class AccountResourceIntTest {

我为每个集成测试设置了唯一的名称。我仍然看不到数据库是新的,因为我只能在日志中看到 Liquibase 校验和。

这是我在 application.yml 中的应用配置

spring:
    datasource:
        driver-class-name: org.h2.Driver
        url: jdbc:h2:mem:myApp;DB_CLOSE_DELAY=-1
        name:
        username:
        password:
    jpa:
        database-platform: com.neustar.registry.le.domain.util.FixedH2Dialect
        database: H2
        open-in-view: false
        show_sql: true
        hibernate:
            ddl-auto: create-drop
            naming-strategy: org.springframework.boot.orm.jpa.hibernate.SpringNamingStrategy
        properties:
            hibernate.cache.use_second_level_cache: false
            hibernate.cache.use_query_cache: false
            hibernate.generate_statistics: true
            hibernate.hbm2ddl.auto: validate

如果重要的话,我的项目是从 JHipster 2.x 版本生成的。请参阅下面的我的数据库配置类。 AppProperties 是应用程序特定的属性(不同于 Spring)。

@Configuration
public class DatabaseConfiguration {

    private static final int LIQUIBASE_POOL_INIT_SIZE = 1;
    private static final int LIQUIBASE_POOL_MAX_ACTIVE = 1;
    private static final int LIQUIBASE_POOL_MAX_IDLE = 0;
    private static final int LIQUIBASE_POOL_MIN_IDLE = 0;

    private static final Logger LOG = LoggerFactory.getLogger(DatabaseConfiguration.class);

    /**
     * Creates data source.
     *
     * @param dataSourceProperties Data source properties configured.
     * @param appProperties the app properties
     * @return Data source.
     */
    @Bean(destroyMethod = "close")
    @ConditionalOnClass(org.apache.tomcat.jdbc.pool.DataSource.class)
    @Primary
    public DataSource dataSource(final DataSourceProperties dataSourceProperties,
        final AppProperties appProperties) {

        LOG.info("Configuring Datasource with url: {}, user: {}",
            dataSourceProperties.getUrl(), dataSourceProperties.getUsername());

        if (dataSourceProperties.getUrl() == null) {
            LOG.error("Your Liquibase configuration is incorrect, please specify database URL!");
            throw new ApplicationContextException("Data source is not configured correctly, please specify URL");
        }
        if (dataSourceProperties.getUsername() == null) {
            LOG.error("Your Liquibase configuration is incorrect, please specify database user!");
            throw new ApplicationContextException(
                "Data source is not configured correctly, please specify database user");
        }
        if (dataSourceProperties.getPassword() == null) {
            LOG.error("Your Liquibase configuration is incorrect, please specify database password!");
            throw new ApplicationContextException(
                "Data source is not configured correctly, "
                    + "please specify database password");
        }

        PoolProperties config = new PoolProperties();
        config.setDriverClassName(dataSourceProperties.getDriverClassName());
        config.setUrl(dataSourceProperties.getUrl());
        config.setUsername(dataSourceProperties.getUsername());
        config.setPassword(dataSourceProperties.getPassword());
        config.setInitialSize(appProperties.getDatasource().getInitialSize());
        config.setMaxActive(appProperties.getDatasource().getMaxActive());
        config.setTestOnBorrow(appProperties.getDatasource().isTestOnBorrow());
        config.setValidationQuery(appProperties.getDatasource().getValidationQuery());

        org.apache.tomcat.jdbc.pool.DataSource dataSource = new org.apache.tomcat.jdbc.pool.DataSource(config);
        LOG.info("Data source is created: {}", dataSource);

        return dataSource;

    }

    /**
     * Create data source for Liquibase using dba user and password provided for "liquibase"
     * in application.yml.
     *
     * @param dataSourceProperties Data source properties
     * @param liquibaseProperties Liquibase properties.
     * @param appProperties the app properties
     * @return Data source for liquibase.
     */
    @Bean(destroyMethod = "close")
    @ConditionalOnClass(org.apache.tomcat.jdbc.pool.DataSource.class)
    public DataSource liquibaseDataSource(final DataSourceProperties dataSourceProperties,
        final LiquibaseProperties liquibaseProperties, final AppProperties appProperties) {

        LOG.info("Configuring Liquibase Datasource with url: {}, user: {}",
            dataSourceProperties.getUrl(), liquibaseProperties.getUser());

        /*
         * This is needed for integration testing. When we run integration tests using SpringJUnit4ClassRunner, Spring
         * uses
         * H2DB if it is in the class path. In that case, we have to create pool for H2DB.
         * Need to find a better solution for this.
         */
        if (dataSourceProperties.getDriverClassName() != null
            && dataSourceProperties.getDriverClassName().startsWith("org.h2.")) {
            return dataSource(dataSourceProperties, appProperties);
        }

        if (dataSourceProperties.getUrl() == null) {
            LOG.error("Your Liquibase configuration is incorrect, please specify database URL!");
            throw new ApplicationContextException("Liquibase is not configured correctly, please specify URL");
        }
        if (liquibaseProperties.getUser() == null) {
            LOG.error("Your Liquibase configuration is incorrect, please specify database user!");
            throw new ApplicationContextException(
                "Liquibase is not configured correctly, please specify database user");
        }
        if (liquibaseProperties.getPassword() == null) {
            LOG.error("Your Liquibase configuration is incorrect, please specify database password!");
            throw new ApplicationContextException(
                "Liquibase is not configured correctly, please specify database password");
        }

        PoolProperties config = new PoolProperties();

        config.setDriverClassName(dataSourceProperties.getDriverClassName());
        config.setUrl(dataSourceProperties.getUrl());
        config.setUsername(liquibaseProperties.getUser());
        config.setPassword(liquibaseProperties.getPassword());

        // for liquibase pool, we dont need more than 1 connection
        config.setInitialSize(LIQUIBASE_POOL_INIT_SIZE);
        config.setMaxActive(LIQUIBASE_POOL_MAX_ACTIVE);

        // for liquibase pool, we dont want any connections to linger around
        config.setMaxIdle(LIQUIBASE_POOL_MAX_IDLE);
        config.setMinIdle(LIQUIBASE_POOL_MIN_IDLE);

        org.apache.tomcat.jdbc.pool.DataSource dataSource = new org.apache.tomcat.jdbc.pool.DataSource(config);
        LOG.info("Liquibase data source is created: {}", dataSource);

        return dataSource;

    }

    /**
     * Creates a liquibase instance.
     *
     * @param dataSource Data source to use for liquibase.
     * @param dataSourceProperties Datasource properties.
     * @param liquibaseProperties Liquibase properties.
     * @return Liquibase instance to be used in spring.
     */
    @Bean
    public SpringLiquibase liquibase(@Qualifier("liquibaseDataSource") final DataSource dataSource,
        final DataSourceProperties dataSourceProperties, final LiquibaseProperties liquibaseProperties) {

        // Use liquibase.integration.spring.SpringLiquibase if you don't want Liquibase to start asynchronously
        SpringLiquibase liquibase = new AsyncSpringLiquibase();
        liquibase.setDataSource(dataSource);
        liquibase.setChangeLog("classpath:config/liquibase/master.xml");
        liquibase.setContexts(liquibaseProperties.getContexts());
        liquibase.setDefaultSchema(liquibaseProperties.getDefaultSchema());
        liquibase.setDropFirst(liquibaseProperties.isDropFirst());
        liquibase.setShouldRun(liquibaseProperties.isEnabled());

        return liquibase;

    }

}

【问题讨论】:

    标签: spring spring-boot h2 jhipster spring-test


    【解决方案1】:

    如果我理解正确,liquibase 会处理数据库状态。对于每个文件,也对于测试数据,liquibase 在表中创建一个校验和,以检查某些内容是否发生了变化。在 @DirtiesContext 之后 h2 实例仍然存在,因此校验和仍然存在于数据库中。 Liquibase 认为一切都是正确的,但测试数据可能已经改变。

    要强制 liquibase 删除数据库并重新创建一个全新的数据库,您必须在 application.yml(用于测试的那个)中设置属性:

    liquibase:
        contexts: test
        drop-first: true
    

    或者作为替代方案,您可以对其进行硬编码:

    liquibase.setDropFirst(true);
    

    您可以使用 @DirtiesContext 注释您的测试,这会减慢测试速度,因为整个应用程序上下文会重新构建。

    或者您可以创建一个更快的自定义 TestExecutionListener。我创建了一个自定义 TestExecutionListener,它会重新创建数据库并保留上下文。

    public class CleanUpDatabaseTestExecutionListener
        extends AbstractTestExecutionListener {
    
        @Inject
        SpringLiquibase liquibase;
    
        @Override
        public int getOrder() {
            return Ordered.HIGHEST_PRECEDENCE;
        }
    
        @Override
        public void afterTestClass(TestContext testContext) throws Exception {
            //This is a bit dirty but it works well
            testContext.getApplicationContext()
                .getAutowireCapableBeanFactory()
                .autowireBean(this);
            liquibase.afterPropertiesSet();
        }
    

    如果您正在使用 TestExecutionListener,则必须将此侦听器添加到您的测试中:

    @RunWith(SpringJUnit4ClassRunner.class)
    @SpringApplicationConfiguration(classes = Application.class)
    @WebAppConfiguration
    @IntegrationTest
    @TestExecutionListeners(listeners = {
        DependencyInjectionTestExecutionListener.class,
        TransactionalTestExecutionListener.class,
        CleanUpDatabaseTestExecutionListener.class,
    })
    public class Test {
        //your tests
    }
    

    注意:不要将@DirtiesContextTestExecutionListener 一起使用,这会导致错误。

    【讨论】:

    • 当您定义多个数据源时,这种方法是一种更好的解决方案。在这种情况下,“spring.datasource.generate-unique-name”不是一个选项。
    【解决方案2】:

    通过去掉usernameurlpassword参数解决。

    spring:
     autoconfigure:
      exclude: org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration
     jackson:
      serialization:
       indent_output: true
     datasource:
      driver-class-name: org.hsqldb.jdbcDriver
      generate-unique-name: true
     jpa:
      hibernate:
        dialect: org.hibernate.dialect.HSQLDialect
        ddl-auto: validate
      show-sql: true
     h2:
      console:
       enabled: false
    
    liquibase:
     change-log: classpath:/liquibase/db.changelog-master.xml
     drop-first: true
     contexts: QA
    

    【讨论】:

      【解决方案3】:

      这是因为每个测试共享同一个数据库,并且 H2 的生命周期不在我们的控制范围内。如果您启动一个进程(VM)并需要一个名为 foo 的数据库,请关闭应用程序上下文,启动一个新的并再次要求 foo,您将获得相同的实例。

      在即将发布的1.4.2 版本中,我们添加了一个属性,用于在启动时为数据库生成一个唯一名称(请参阅spring.datasource.generate-unique-name),该值将在 1.5 上默认设置为 true。

      与此同时,您可以使用 @SpringBootTest(properties="spring.datasource.name=xyz") 注释每个测试,其中 xyz 对于需要单独数据库的测试是不同的。

      【讨论】:

      • 感谢您的快速回复。不幸的是,这种改变对我不起作用。可能是因为我配置数据库的方式?我用更多信息更新了原始帖子。
      • 是的,看起来 JHipster 完全忽略了该属性,而且您也没有使用自动配置。尝试将 jhipster 标签添加到您的问题中以引起更多关注。
      • 我用spring.datasource.url 代替了名字。我现在可以在集成测试中硬编码。我想在数据库配置中配置它,但我有两个数据源连接到同一个数据库但具有不同的权限,并且很难为它们两个传递数据库名称的随机值。也许我可以在数据库配置类中尝试一个静态变量。感谢您的帮助。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2022-10-18
      • 2012-01-06
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-12-01
      相关资源
      最近更新 更多