如何在 Spring 集成测试之间删除内存中的 h2db?
How to drop in-memory h2db between Spring Integration tests?
我在 Spring 网络应用程序中使用 Liquibase。我有一堆实体,在针对每个实体(如用户、帐户、发票、许可证等)的集成测试中对 REST API 进行了数百次测试。当 运行 by class 时,我的所有集成测试都通过了,但是当 运行 一起使用 gradle test
时,很多都失败了。测试之间很可能存在数据冲突,我现在对花时间修复清理数据不感兴趣。我更喜欢在每个 class 之后删除数据库和上下文。我想我可以在 class 处使用 @DirtiesContext
,所以我用它进行了测试注释。
@RunWith(SpringRunner.class)
@SpringBootTest(classes = {Application.class, SecurityConfiguration.class},
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@DirtiesContext
public class InvoiceResourceIntTest {
我看到添加注释后,Web 应用程序上下文会在每个 class 时启动,但是当 Liquibase 初始化发生时,查询不是 运行,因为校验和匹配。由于这是一个内存数据库,我预计数据库会与 spring 上下文一起被销毁,但它没有发生。
我还将 jpa hibernate ddl-auto 设置为 create-drop
但这没有帮助。我正在考虑的下一个选项是代替 mem
,将 h2db 写入文件并在我的集成测试 class 文件的 @BeforeClass 中删除该文件。我更喜欢在内存中自动删除数据库而不是在测试中管理它,但想在这里尝试作为最后的选择。感谢您的帮助。
更新:
我更新测试如下。
@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 版本生成的。请在下面查看我的数据库配置 class。 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;
}
}
这是因为每个测试共享同一个数据库,H2 的生命周期不受我们控制。如果您启动一个进程(VM)并需要一个名为 foo
的数据库,请关闭应用程序上下文,启动一个新进程并再次需要 foo
您将获得相同的实例。
在即将发布的 1.4.2
版本中,我们添加了一个 属性 以在启动时为数据库生成一个唯一的名称(参见 spring.datasource.generate-unique-name
)并且该值将设置为 true默认为 1.5.
同时,您可以用 @SpringBootTest(properties="spring.datasource.name=xyz")
注释每个测试,其中 xyz
对于需要单独数据库的测试是不同的。
如果我理解正确的话,liquibase 会处理数据库状态。对于每个文件,也包括测试数据,liquibase 在 table 中创建一个校验和来检查是否发生了某些变化。 h2 实例在 @DirtiesContext 之后仍然存在,因此校验和仍然存在于数据库中。 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
}
注意:不要同时使用 @DirtiesContext
和 TestExecutionListener
,这会导致错误。
通过删除 username
、url
和 password
参数解决。
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
我在 Spring 网络应用程序中使用 Liquibase。我有一堆实体,在针对每个实体(如用户、帐户、发票、许可证等)的集成测试中对 REST API 进行了数百次测试。当 运行 by class 时,我的所有集成测试都通过了,但是当 运行 一起使用 gradle test
时,很多都失败了。测试之间很可能存在数据冲突,我现在对花时间修复清理数据不感兴趣。我更喜欢在每个 class 之后删除数据库和上下文。我想我可以在 class 处使用 @DirtiesContext
,所以我用它进行了测试注释。
@RunWith(SpringRunner.class)
@SpringBootTest(classes = {Application.class, SecurityConfiguration.class},
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@DirtiesContext
public class InvoiceResourceIntTest {
我看到添加注释后,Web 应用程序上下文会在每个 class 时启动,但是当 Liquibase 初始化发生时,查询不是 运行,因为校验和匹配。由于这是一个内存数据库,我预计数据库会与 spring 上下文一起被销毁,但它没有发生。
我还将 jpa hibernate ddl-auto 设置为 create-drop
但这没有帮助。我正在考虑的下一个选项是代替 mem
,将 h2db 写入文件并在我的集成测试 class 文件的 @BeforeClass 中删除该文件。我更喜欢在内存中自动删除数据库而不是在测试中管理它,但想在这里尝试作为最后的选择。感谢您的帮助。
更新:
我更新测试如下。
@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 版本生成的。请在下面查看我的数据库配置 class。 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;
}
}
这是因为每个测试共享同一个数据库,H2 的生命周期不受我们控制。如果您启动一个进程(VM)并需要一个名为 foo
的数据库,请关闭应用程序上下文,启动一个新进程并再次需要 foo
您将获得相同的实例。
在即将发布的 1.4.2
版本中,我们添加了一个 属性 以在启动时为数据库生成一个唯一的名称(参见 spring.datasource.generate-unique-name
)并且该值将设置为 true默认为 1.5.
同时,您可以用 @SpringBootTest(properties="spring.datasource.name=xyz")
注释每个测试,其中 xyz
对于需要单独数据库的测试是不同的。
如果我理解正确的话,liquibase 会处理数据库状态。对于每个文件,也包括测试数据,liquibase 在 table 中创建一个校验和来检查是否发生了某些变化。 h2 实例在 @DirtiesContext 之后仍然存在,因此校验和仍然存在于数据库中。 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
}
注意:不要同时使用 @DirtiesContext
和 TestExecutionListener
,这会导致错误。
通过删除 username
、url
和 password
参数解决。
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