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

时间:2016-01-05 16:59:16

标签: java spring spring-mvc spring-boot spring-test

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

11 个答案:

答案 0 :(得分:61)

实际上,我认为你想要这个:

@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,即上下文   在课堂上的每个测试方法后都会被标记为脏。

答案 1 :(得分:8)

要创建数据库,您必须使用spring.jpa.hibernate.ddl-auto=create-drop执行其他答案所说的内容,现在,如果您的目的是在每个测试中对数据库进行傀儡,那么spring提供了非常有用的anotation

@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;,您可以运行before测试方法和after测试方法。填充数据库。

关于每次创建数据库,假设您只希望测试具有create-drop选项,您可以使用带有此批注的自定义属性配置测试

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

希望有所帮助

答案 2 :(得分: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)

答案 3 :(得分:4)

如果使用spring.jpa.hibernate.ddl-auto=create-drop应该足以创建/删除数据库?

答案 4 :(得分:3)

使用Spring-Boot 2.2.0中可接受的答案,我看到与约束有关的JDBC语法错误:

  

由以下原因引起:org.h2.jdbc.JdbcSQLSyntaxErrorException:约束“ FKEFFD698EA2E75FXEERWBO8IUT”已经存在; SQL语句:   更改表foo添加约束FKeffd698ea2e75fxeerwbo8iut外键(栏)引用栏[90045-200]

为解决此问题,我在单元测试中添加了@AutoConfiguredTestDatabase

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 { ... }

答案 5 :(得分:2)

除非你使用某种Spring-Data集成(我根本不知道),这似乎是你需要自己实现的自定义逻辑。 Spring不了解您的数据库,其模式和表。

假设使用JUnit,请编写适当的@Before@After方法来设置和清理数据库,其表和数据。您的测试本身可以编写他们需要的数据,并在适当的情况下自行清理。

答案 6 :(得分:1)

如果您正在寻找class Driver的替代方法,则下面的代码将为您提供帮助。我使用了this answer中的一些代码。

首先,在测试资源文件夹的Handler文件上设置H2数据库:

@DirtiesContext

然后,创建一个名为application.yml的类:

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

上面的代码将重置数据库(截断表,重置序列等),并且仅适用于H2。如果您正在使用其他内存数据库(例如HsqlDB),则需要对SQL进行必要的更改以完成相同的操作。

之后,转到测试类并添加ResetDatabaseTestExecutionListener批注,例如:

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

这应该有效。

老实说,使用这种方法或@TestExecutionListeners不会看到任何性能差异,但是也许在更大的应用程序中,这可以提高集成测试性能。

答案 7 :(得分:1)

使用 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 {

答案 8 :(得分:0)

您可以使用@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
    }
}

所有测试都包装在一个事务中,该事务将在每个测试结束时回滚。不幸的是,该注释当然存在一些问题,例如当您的生产代码使用具有不同分数的事务时,您需要特别注意。

答案 9 :(得分:0)

对我没有任何作用,但以下内容: 对于每个测试类,您可以添加以下注释:

@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 的操作而失败。

答案 10 :(得分:-1)

您还可以尝试使用https://www.testcontainers.org/,它可以帮助您在容器内运行数据库,也可以为每次测试运行创建一个新的数据库。但是,这将非常慢,因为每次必须创建一个容器并且必须启动,配置数据库服务器,然后必须运行迁移,然后才能执行测试。