如何在每次测试前重新创建数据库?

我的 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,它在每次测试之前都没有调用。为什么?

156914 次浏览

Unless you're using some kind of Spring-Data integration (which I don't know at all), this seems like custom logic you'll need to implement yourself. Spring doesn't know about your databases, its schemas, and tables.

Assuming JUnit, write appropriate @Before and @After methods to set up and clean up your database, its tables, and data. Your tests can themselves write the data they need, and potentially clean up after themselves if appropriate.

If you use spring.jpa.hibernate.ddl-auto=create-drop should be enough to create/drop database?

To create the database you have to do what the other answers say with the spring.jpa.hibernate.ddl-auto=create-drop, now if your intent is to pupulate the database on each test then spring provides a very usefull anotation

@Transactional(value=JpaConfiguration.TRANSACTION_MANAGER_NAME)
@Sql(executionPhase=ExecutionPhase.BEFORE_TEST_METHOD,scripts="classpath:/test-sql/group2.sql")
public class GroupServiceTest extends TimeoffApplicationTests {

that is from this package org.springframework.test.context.jdbc.Sql; and you can run a before test method and a after test method. To populate the database.

Regarding creating the database each time, Say you only want your Test to have the create-drop option you can configure your tests with a custom properties with this annotation

@TestPropertySource(locations="classpath:application-test.properties")
public class TimeoffApplicationTests extends AbstractTransactionalJUnit4SpringContextTests{

Hope it helps

Actually, I think you want this:

@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 may be used as a class-level and method-level annotation within the same class. In such scenarios, the ApplicationContext will be marked as dirty after any such annotated method as well as after the entire class. If the DirtiesContext.ClassMode is set to AFTER_EACH_TEST_METHOD, the context will be marked dirty after each test method in the class.

You put it on your Test class.

With spring boot the h2 database can be defined uniquely for each test. Just override the data source URL for each test

 @SpringBootTest(properties = {"spring.config.name=myapp-test-h2","myapp.trx.datasource.url=jdbc:h2:mem:trxServiceStatus"})

The tests can run in parallel.

Within the test the data can be reset by

@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD)

If you are looking for an alternative for the @DirtiesContext, this code below will help you. I used some code from this answer.

First, setup the H2 database on the application.yml file on your test resources folder:

spring:
datasource:
platform: h2
url: jdbc:h2:mem:test
driver-class-name: org.h2.Driver
username: sa
password:

After that, create a class called 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();
}
}

The code above will reset the database (truncate tables, reset sequences, etc) and is prepared to work with H2 database. If you are using another memory database (like HsqlDB) you need to make the necessary changes on the SQLs queries to accomplish the same thing.

After that, go to your test class and add the @TestExecutionListeners annotation, like:

@TestExecutionListeners(mergeMode =
TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS,
listeners = {ResetDatabaseTestExecutionListener.class}
)
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class CreateOrderIT {

This should work.

If you not see any performance difference between this approach and @DirtiesContext, probably you are using @MockBean in your tests, what marks the Spring context as dirty and automatically reload the entire context.

You can annotate your test class with @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
}
}

All tests are wrapped inside a transaction, that is rolled back at the end of each test. There are unfortunately some problems with that annotation of course, and you need to pay special attention, when for example your production code uses transactions with different score.

Using the accepted answer in Spring-Boot 2.2.0, I was seeing JDBC syntax errors related to constraints:

Caused by: org.h2.jdbc.JdbcSQLSyntaxErrorException: Constraint "FKEFFD698EA2E75FXEERWBO8IUT" already exists; SQL statement: alter table foo add constraint FKeffd698ea2e75fxeerwbo8iut foreign key (bar) references bar [90045-200]

To fix this, I added @AutoConfigureTestDatabase to my unit test (part of 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 { ... }

You could also try out https://www.testcontainers.org/ which helps you to run databases inside containers and you can create a fresh database for each test run too. It will be very slow though, since each time a container has to be created and the database server has to be started, configured and then migrations have to be run, then the test can be executed.

A solution using try/resources and a configurable schema based on this answer. Our trouble was that our H2 database leaked data between test cases. So this Listener fires before each test method.

The 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);
}
}
}

Using a custom annotation:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@TestExecutionListeners(mergeMode =
TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS,
listeners = { ResetDatabaseTestExecutionListener.class }
)
public @interface ResetDatabase {
}

You can easily mark each test in which you want to reset the database:

@SpringBootTest(
webEnvironment = RANDOM_PORT,
classes = { Application.class }
)
@ResetDatabase
public class SomeClassIT {

Nothing worked for me, but the following: For every test class you can put the following annotations:

@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

To order specific tests within the class you have to put also @Order annotation:

@Test
@Order(1) //first test
@Test
@Order(2) //second test, etc.

Rerunning the tests will not fail because of previous manipulations with db.

There is library that covers "reset H2 database" feature in JUnit 5 tests:

https://github.com/cronn/test-utils#h2util

Sample usage:

@ExtendWith(SpringExtension.class)
@Import(H2Util.class)
class MyTest {


@BeforeEach
void resetDatabase(@Autowired H2Util h2Util) {
h2Util.resetDatabase();
}


// tests...
}

Maven coords:

<dependency>
<groupId>de.cronn</groupId>
<artifactId>test-utils</artifactId>
<version>0.2.0</version>
<scope>test</scope>
</dependency>

Disclaimer: I’m the author of suggested library.