【问题标题】:How to re-create database before each test in Spring?如何在 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 我知道这很短,但你的评论不应该是一个答案吗?

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


【解决方案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@DirtiesContextprobably@DirtiesContext 之间没有发现任何性能差异,那么是什么将Spring 上下文标记为脏并自动重新加载整个上下文。

    【讨论】:

    【解决方案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 的操作而失败。

            【讨论】:

              猜你喜欢
              • 2010-10-30
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2015-04-26
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多