如何在 Spring 中的每次测试之前重新创建数据库?
Posted
技术标签:
【中文标题】如何在 Spring 中的每次测试之前重新创建数据库?【英文标题】:How to re-create database before each test in Spring? 【发布时间】:2016-04-09 14:24:43 【问题描述】:我的 Spring-Boot-Mvc-Web 应用程序在 application.properties
文件中有以下数据库配置:
spring.datasource.url=jdbc:h2:tcp://localhost/~/pdk
spring.datasource.username=sa
spring.datasource.password=
spring.datasource.driver-class-name=org.h2.Driver
这是我做的唯一配置。我在任何地方都没有任何其他配置。尽管如此,Spring 和子系统会在每个 Web 应用程序运行时自动重新创建数据库。数据库在系统运行时重新创建,而它包含应用程序结束后的数据。
我不理解这个默认值,并期望这适合测试。
但是当我开始运行测试时,我发现数据库只重新创建了一次。由于测试没有按预定义的顺序执行,这完全没有意义。
所以,问题是:如何理解?即如何在每次测试之前重新创建数据库,因为它在应用程序首次启动时发生?
我的测试类标题如下:
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = myapp.class)
//@WebAppConfiguration
@WebIntegrationTest
@DirtiesContext
public class WebControllersTest
如您所见,我在课堂级别尝试了@DirtiesContext
,但没有帮助。
更新
我有一颗豆子
@Service
public class DatabaseService implements InitializingBean
有一个方法
@Override
@Transactional()
public void afterPropertiesSet() throws Exception
log.info("Bootstrapping data...");
User user = createRootUser();
if(populateDemo)
populateDemos();
log.info("...Bootstrapping completed");
现在我使用populateDemos()
方法来清除数据库中的所有数据。不幸的是,尽管@DirtiesContext
,它并没有在每次测试之前调用。为什么?
【问题讨论】:
这是自定义逻辑。 Spring 对您的数据库一无所知。写一个@Before
和@After
来设置和清理。
@SotiriosDelimanolis 我知道这很短,但你的评论不应该是一个答案吗?
【参考方案1】:
其实,我想你想要这个:
@DirtiesContext(classMode = ClassMode.BEFORE_EACH_TEST_METHOD)
http://docs.spring.io/autorepo/docs/spring-framework/4.2.6.RELEASE/javadoc-api/org/springframework/test/annotation/DirtiesContext.html
@DirtiesContext 可以用作类级别和方法级别 同一类中的注释。在这种情况下, 在任何此类注释之后,ApplicationContext 将被标记为脏 方法以及整个课程之后。如果 DirtiesContext.ClassMode 设置为 AFTER_EACH_TEST_METHOD,上下文 在类中的每个测试方法之后都会被标记为脏。
你把它放在你的测试课上。
【讨论】:
它不会给出重复的键违规,因为它会重新创建数据库,而不仅仅是删除表中的所有值。它删除数据库。因此,每个测试都将使用全新的数据库运行。这样,一个测试就不会影响另一个。 由于某种原因它没有清除我在内存数据库中的h2
。
@lapots 我只是在调查类似的问题并找到了解决方案,但是不能说为什么它对我有用。我的设置是:Spring-boot5、junit5、内存中的 H2、类级别的 DirtiesContext。我发现当 H2 url 命名为 'jdbc:h2:mem:mem1' (mem1 在这里很重要)时,测试失败(mvn test)。但是让 H2 url 像 'jdbc:h2:mem' 这样匿名可以解决这个问题!
如果你使用 ClassMode.BEFORE_EACH_TEST_METHOD 确保使用 @TestExecutionListeners(DirtiesContextBeforeModesTestExecutionListener.class,...) 否则不支持。
我认为脏上下文对性能的影响非常大。通常只有在每次测试后回滚事务以重置数据库。【参考方案2】:
使用 Spring-Boot 2.2.0 中接受的答案,我看到了与约束相关的 JDBC 语法错误:
原因:org.h2.jdbc.JdbcSQLSyntaxErrorException:约束“FKEFFD698EA2E75FXEERWBO8IUT”已经存在; SQL 语句: 更改表 foo 添加约束 FKeffd698ea2e75fxeerwbo8iut 外键(bar)引用 bar [90045-200]
为了解决这个问题,我在单元测试中添加了@AutoConfigureTestDatabase
(spring-boot-test-autoconfigure 的一部分):
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase.Replace;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.annotation.DirtiesContext.ClassMode;
import org.springframework.boot.test.context.SpringBootTest;
import org.junit.runner.RunWith;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
@DirtiesContext(classMode = ClassMode.BEFORE_EACH_TEST_METHOD)
@AutoConfigureTestDatabase(replace = Replace.ANY)
public class FooRepositoryTest ...
【讨论】:
升级到 Spring-Boot 2.2.x 后,我有了这个表面。我只希望我能不止一次地对此表示赞同。浪费了半天时间试图弄清楚如何解决这个问题。【参考方案3】:要创建数据库,您必须使用 spring.jpa.hibernate.ddl-auto=create-drop
执行其他答案所说的操作,现在如果您的意图是在每次测试中填充数据库,那么 spring 提供了一个非常有用的注释
@Transactional(value=JpaConfiguration.TRANSACTION_MANAGER_NAME)
@Sql(executionPhase=ExecutionPhase.BEFORE_TEST_METHOD,scripts="classpath:/test-sql/group2.sql")
public class GroupServiceTest extends TimeoffApplicationTests
来自这个包org.springframework.test.context.jdbc.Sql;
,您可以运行前测试方法和后测试方法。填充数据库。
关于每次创建数据库,假设您只希望您的测试具有 create-drop 选项,您可以使用带有此注释的自定义属性配置您的测试
@TestPropertySource(locations="classpath:application-test.properties")
public class TimeoffApplicationTests extends AbstractTransactionalJUnit4SpringContextTests
希望对你有帮助
【讨论】:
【参考方案4】:如果您正在寻找@DirtiesContext
的替代方案,下面的代码将为您提供帮助。我使用了this answer的一些代码。
首先,在您的测试资源文件夹中的application.yml
文件中设置H2 数据库:
spring:
datasource:
platform: h2
url: jdbc:h2:mem:test
driver-class-name: org.h2.Driver
username: sa
password:
之后,创建一个名为ResetDatabaseTestExecutionListener
的类:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.TestContext;
import org.springframework.test.context.support.AbstractTestExecutionListener;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.HashSet;
import java.util.Set;
public class ResetDatabaseTestExecutionListener extends AbstractTestExecutionListener
@Autowired
private DataSource dataSource;
public final int getOrder()
return 2001;
private boolean alreadyCleared = false;
@Override
public void beforeTestClass(TestContext testContext)
testContext.getApplicationContext()
.getAutowireCapableBeanFactory()
.autowireBean(this);
@Override
public void prepareTestInstance(TestContext testContext) throws Exception
if (!alreadyCleared)
cleanupDatabase();
alreadyCleared = true;
@Override
public void afterTestClass(TestContext testContext) throws Exception
cleanupDatabase();
private void cleanupDatabase() throws SQLException
Connection c = dataSource.getConnection();
Statement s = c.createStatement();
// Disable FK
s.execute("SET REFERENTIAL_INTEGRITY FALSE");
// Find all tables and truncate them
Set<String> tables = new HashSet<>();
ResultSet rs = s.executeQuery("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES where TABLE_SCHEMA='PUBLIC'");
while (rs.next())
tables.add(rs.getString(1));
rs.close();
for (String table : tables)
s.executeUpdate("TRUNCATE TABLE " + table);
// Idem for sequences
Set<String> sequences = new HashSet<>();
rs = s.executeQuery("SELECT SEQUENCE_NAME FROM INFORMATION_SCHEMA.SEQUENCES WHERE SEQUENCE_SCHEMA='PUBLIC'");
while (rs.next())
sequences.add(rs.getString(1));
rs.close();
for (String seq : sequences)
s.executeUpdate("ALTER SEQUENCE " + seq + " RESTART WITH 1");
// Enable FK
s.execute("SET REFERENTIAL_INTEGRITY TRUE");
s.close();
c.close();
上面的代码将重置数据库(截断表、重置序列等)并准备好与 H2 数据库一起使用。如果您使用的是另一个内存数据库(如 HsqlDB),则需要对 SQL 查询进行必要的更改以完成相同的操作。
之后,转到您的测试类并添加 @TestExecutionListeners
注释,例如:
@TestExecutionListeners(mergeMode =
TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS,
listeners = ResetDatabaseTestExecutionListener.class
)
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class CreateOrderIT
这应该可行。
如果您在测试中使用@MockBean
与@DirtiesContext
、probably 和@DirtiesContext
之间没有发现任何性能差异,那么是什么将Spring 上下文标记为脏并自动重新加载整个上下文。
【讨论】:
这对我们有用!我已经对其进行了调整以供我们使用,您可以在此处查看代码:***.com/a/67262467/2630810【参考方案5】:使用 spring boot,可以为每个测试单独定义 h2 数据库。只需覆盖每个测试的数据源 URL
@SpringBootTest(properties = "spring.config.name=myapp-test-h2","myapp.trx.datasource.url=jdbc:h2:mem:trxServiceStatus")
测试可以并行运行。
在测试中可以重置数据
@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD)
【讨论】:
【参考方案6】:如果你使用spring.jpa.hibernate.ddl-auto=create-drop
应该足以创建/删除数据库?
【讨论】:
这个可能是Spring默认使用的,原因不是很清楚。 Drop-create 仅在 JVM 实际退出时才有用 - 如果您有多个测试类并且想要在这些测试类之间删除和创建,这将不起作用【参考方案7】:除非您使用某种 Spring-Data 集成(我根本不知道),否则这似乎是您需要自己实现的自定义逻辑。 Spring 不知道您的数据库、它的模式和表。
假设 JUnit,编写适当的 @Before
和 @After
方法来设置和清理您的数据库、它的表和数据。您的测试可以自己编写所需的数据,并在适当时自行清理。
【讨论】:
但是谁在程序启动时删除了当前的数据库?如果逻辑是自定义的,那么为什么它已经在没有我明确命令的情况下清除数据库? 它不在内存数据库中,因为 url 是dbc:h2:tcp://localhost/~/pdk
。它是真正的数据库,我可以看到它的文件并与数据库工具分开访问它。它可能被设置为create
或drop-create
的底层Hibernate 默认配置删除。问题是是否可以不明确地重新启动...【参考方案8】:
在 JUnit 5 测试中有一个涵盖“重置 H2 数据库”功能的库:
https://github.com/cronn/test-utils#h2util
示例用法:
@ExtendWith(SpringExtension.class)
@Import(H2Util.class)
class MyTest
@BeforeEach
void resetDatabase(@Autowired H2Util h2Util)
h2Util.resetDatabase();
// tests...
Maven 坐标:
<dependency>
<groupId>de.cronn</groupId>
<artifactId>test-utils</artifactId>
<version>0.2.0</version>
<scope>test</scope>
</dependency>
免责声明:我是建议库的作者。
【讨论】:
【参考方案9】:使用 try/resources 和基于 this answer 的可配置架构的解决方案。我们的问题是我们的 H2 数据库在测试用例之间泄露了数据。所以这个Listener
在每个测试方法之前触发。
Listener
:
public class ResetDatabaseTestExecutionListener extends AbstractTestExecutionListener
private static final List<String> IGNORED_TABLES = List.of(
"TABLE_A",
"TABLE_B"
);
private static final String SQL_DISABLE_REFERENTIAL_INTEGRITY = "SET REFERENTIAL_INTEGRITY FALSE";
private static final String SQL_ENABLE_REFERENTIAL_INTEGRITY = "SET REFERENTIAL_INTEGRITY TRUE";
private static final String SQL_FIND_TABLE_NAMES = "SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES where TABLE_SCHEMA='%s'";
private static final String SQL_TRUNCATE_TABLE = "TRUNCATE TABLE %s.%s";
private static final String SQL_FIND_SEQUENCE_NAMES = "SELECT SEQUENCE_NAME FROM INFORMATION_SCHEMA.SEQUENCES WHERE SEQUENCE_SCHEMA='%s'";
private static final String SQL_RESTART_SEQUENCE = "ALTER SEQUENCE %s.%s RESTART WITH 1";
@Autowired
private DataSource dataSource;
@Value("$schema.property")
private String schema;
@Override
public void beforeTestClass(TestContext testContext)
testContext.getApplicationContext()
.getAutowireCapableBeanFactory()
.autowireBean(this);
@Override
public void beforeTestMethod(TestContext testContext) throws Exception
cleanupDatabase();
private void cleanupDatabase() throws SQLException
try (
Connection connection = dataSource.getConnection();
Statement statement = connection.createStatement()
)
statement.execute(SQL_DISABLE_REFERENTIAL_INTEGRITY);
Set<String> tables = new HashSet<>();
try (ResultSet resultSet = statement.executeQuery(String.format(SQL_FIND_TABLE_NAMES, schema)))
while (resultSet.next())
tables.add(resultSet.getString(1));
for (String table : tables)
if (!IGNORED_TABLES.contains(table))
statement.executeUpdate(String.format(SQL_TRUNCATE_TABLE, schema, table));
Set<String> sequences = new HashSet<>();
try (ResultSet resultSet = statement.executeQuery(String.format(SQL_FIND_SEQUENCE_NAMES, schema)))
while (resultSet.next())
sequences.add(resultSet.getString(1));
for (String sequence : sequences)
statement.executeUpdate(String.format(SQL_RESTART_SEQUENCE, schema, sequence));
statement.execute(SQL_ENABLE_REFERENTIAL_INTEGRITY);
使用自定义注释:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@TestExecutionListeners(mergeMode =
TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS,
listeners = ResetDatabaseTestExecutionListener.class
)
public @interface ResetDatabase
您可以轻松地标记要在其中重置数据库的每个测试:
@SpringBootTest(
webEnvironment = RANDOM_PORT,
classes = Application.class
)
@ResetDatabase
public class SomeClassIT
【讨论】:
【参考方案10】:你可以用@Transactional
注释你的测试类:
import org.springframework.transaction.annotation.Transactional;
...
...
@RunWith(SpringRunner.class)
@Transactional
public class MyClassTest
@Autowired
private SomeRepository repository;
@Before
public void init()
// add some test data, that data would be rolled back, and recreated for each separate test
repository.save(...);
@Test
public void testSomething()
// add some more data
repository.save(...);
// update some base data
repository.delete(...);
// all the changes on database done in that test would be rolled back after test finish
所有测试都包装在一个事务中,该事务在每个测试结束时回滚。不幸的是,该注释当然存在一些问题,您需要特别注意,例如当您的生产代码使用不同分数的事务时。
【讨论】:
我不知道为什么,但是当我使用@Transactional 时,同一测试方法内的操作在方法范围内是不可见的。例如,我在 DB 中添加了一个元素(并获得了分配给它的序列的 Id 值),但就在我从 DB 中查询相同的项目之后,它不可用。【参考方案11】:您也可以尝试https://www.testcontainers.org/,它可以帮助您在容器内运行数据库,并且您也可以为每次测试运行创建一个新数据库。不过会很慢,因为每次都要创建一个容器,启动数据库服务器,配置好,然后运行迁移,然后才能执行测试。
【讨论】:
【参考方案12】:没有什么对我有用,但以下内容: 对于每个测试类,您可以添加以下注释:
@TestMethodOrder(MethodOrderer.OrderAnnotation.class) //in case you need tests to be in specific order
@DataJpaTest // will disable full auto-configuration and instead apply only configuration relevant to JPA tests
@AutoConfigureTestDatabase(replace = NONE) //configures a test database to use instead of the application-defined or auto-configured DataSource
要对类中的特定测试进行排序,您还必须添加 @Order 注释:
@Test
@Order(1) //first test
@Test
@Order(2) //second test, etc.
重新运行测试不会因为之前对 db 的操作而失败。
【讨论】:
以上是关于如何在 Spring 中的每次测试之前重新创建数据库?的主要内容,如果未能解决你的问题,请参考以下文章
Codeception CEST 接受测试,如何在每次测试之前重新运行 __bootstrap 代码?
在 Spring Boot 中,如何在每次测试之前重置指标注册表?
VS2010 负载测试:如何执行在每次负载测试之前运行一次的自定义操作