如何在 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。它是真正的数据库,我可以看到它的文件并与数据库工具分开访问它。它可能被设置为createdrop-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 中,如何在每次测试之前重置指标注册表?

Spring H2 Test DB 在每次测试之前不会重置

VS2010 负载测试:如何执行在每次负载测试之前运行一次的自定义操作

带有 DB 的 Spring Boot 应用程序 - 使用 @DirtiesContext 重新创建上下文后测试类失败

如何从 PHPUnit 测试设置运行 Laravel 数据库播种机?