【问题标题】:How do I reset my database state after each unit test without making the whole test a transaction?如何在每次单元测试后重置我的数据库状态而不使整个测试成为事务?
【发布时间】:2012-12-29 21:57:42
【问题描述】:

我正在使用 Spring 3.1.1.RELEASE、Hibernate 4.1.0.Final、JPA 2、JUnit 4.8.1 和 HSQL 2.2.7。我想在我的服务方法上运行一些 JUnit 测试,并且在每次测试之后,我希望回滚写入内存数据库的任何数据。但是,我不希望将整个测试视为事务。例如在这个测试中

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({ "classpath:test-context.xml" })
public class ContractServiceTest 
{
    …

    @Autowired
    private ContractService m_contractService;

    @Test
    public void testUpdateContract()
    {
        // Add the contract
        m_contractService.save(m_contract);
        Assert.assertNotNull(m_contract.getId());
        // Update the activation date by 6 months.
        final Calendar activationDate = Calendar.getInstance();
        activationDate.setTime(activationDate.getTime());
        activationDate.add(Calendar.MONTH, 6);
        m_contract.setActivationDate(activationDate.getTime());
        m_contractService.save(m_contract);
        final List<Contract> foundContracts = m_contractService.findContractByOppId(m_contract.getOpportunityId());
        Assert.assertEquals(foundContracts.get(0), m_contract);
    }   // testUpdateContract

对服务有三个调用(“m_contractService.save”、“m_contractService.save”和“m_contractService.findContractByOppId”),每个都被视为我想要的事务。但我不知道如何在每次单元测试后将我的内存数据库重置为原始状态。

如果我需要提供其他信息,请告诉我。

【问题讨论】:

标签: spring junit transactions hsqldb


【解决方案1】:

从 JUnit 5 开始,您还可以创建自定义扩展并从 Spring 上下文访问数据源,如下所示(使用 Kotlin):

class DatabaseCleanerExtension : AfterEachCallback {
  override fun afterEach(context: ExtensionContext) {
    val ds = SpringExtension.getApplicationContext(context).getBean(DataSource::class.java)

    ds.connection.use { connection ->
      connection.prepareStatement("DELETE FROM my_table").execute()
    }
  }
}

然后您可以按如下方式注册扩展:

@SpringBootTest
@ExtendWith(DatabaseCleanerExtension::class)
class SpringJunitExtensionApplicationTests { .. }

现在在每次测试后都会执行回调,您可以轻松地注释任何适用的测试类。

Here is also a video on settings this up.

【讨论】:

    【解决方案2】:

    由于您使用的是 Hibernate,您可以使用属性hibernate.hbm2ddl.auto 在每次启动时创建数据库。您还需要在每次测试后强制重新加载弹簧上下文。您可以使用 @DirtiesContext 注释来做到这一点。

    这可能会为您的测试增加一点额外开销,因此另一种解决方案是手动删除每个表中的数据。

    【讨论】:

    • 我在课堂级别添加了“@DirtiesContext(classMode = ClassMode.AFTER_EACH_TEST_METHOD)”,它满足了我的需求。有额外的开销,但在我的情况下这并不重要。摇滚
    • 是的,我必须明确设置 classMode = ClassMode.AFTER_EACH_TEST_METHOD)。默认是“课后”,但我想要一个新的数据库准备好到达单元测试方法。
    【解决方案3】:

    如果您使用 flyway 进行迁移,我使用以下模式:

    @TestInstance(TestInstance.Lifecycle.PER_CLASS)
    class JUnit5Class {
        @Autowired
        Flyway flyway;
    
        @BeforeAll
        public void cleanUp(){
            flyway.clean();
            flyway.migrate();
        }
    }
    

    @TestInstance 允许您将@BeforeAll 设为非静态,因此每个测试类只能迁移一次。如果您想为每个测试重置它,请删除类注释并将@BeforeAll 更改为@BeforeEach。

    【讨论】:

      【解决方案4】:

      我为每个测试使用随机内存数据库解决了同样的问题:

      @DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD)
      @TestPropertySource(properties = {
          "spring.datasource.url=jdbc:hsqldb:mem:${random.uuid}"
      })
      

      【讨论】:

        【解决方案5】:

        @DirtiesContext 对我来说没有解决方案,因为整个应用程序上下文都被破坏了,必须在每次测试后创建 -> 花了很长时间。

        @Before 对我来说也不是一个好的解决方案,因为我必须在每个集成测试中创建 @Before。

        所以我决定创建一个TestExecutionListener,它会在每次测试后重新创建数据库。 (使用 Liquibase,但它也适用于 Flyway 和普通 SQL)

        public class CleanupDatabaseTestExecutionListener
        extends AbstractTestExecutionListener {
        
        public final int getOrder() {
            return 2001;
        }
        
        private boolean alreadyCleared = false;
        
        @Override
        public void prepareTestInstance(TestContext testContext) throws Exception {
            if (!alreadyCleared) {
                cleanupDatabase(testContext);
                alreadyCleared = true;
            } else {
                alreadyCleared = true;
            }
        }
        
        @Override
        public void afterTestClass(TestContext testContext) throws Exception {
            cleanupDatabase(testContext);
        }
        
        private void cleanupDatabase(TestContext testContext) throws LiquibaseException {
            ApplicationContext app = testContext.getApplicationContext();
            SpringLiquibase springLiquibase = app.getBean(SpringLiquibase.class);
            springLiquibase.setDropFirst(true);
            springLiquibase.afterPropertiesSet(); //The database get recreated here
        }
        }
        

        为了使用 TestExecutionListenere,我创建了一个自定义测试注释

        @Retention(RetentionPolicy.RUNTIME)
        @Target(ElementType.TYPE)
        @RunWith(SpringRunner.class)
        @SpringBootTest(classes = OurderApp.class)
        @TestExecutionListeners(mergeMode = 
        TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS,
            listeners = {CleanupDatabaseTestExecutionListener.class}
        )
        public @interface OurderTest {
        }
        

        最后但同样重要的是,我现在可以创建测试并且可以确定数据库处于干净模式。

        @RunWith(SpringRunner.class)
        @OurderTest
        public class ProductSaveServiceIntTest {
         }
        

        编辑:我稍微改进了我的解决方案。我有一个问题,有时一种测试方法破坏了我的数据库,用于测试类中所有即将进行的测试。所以我创建了注释

        包 com.ourder.e2e.utils;

        import java.lang.annotation.ElementType;
        import java.lang.annotation.Retention;
        import java.lang.annotation.RetentionPolicy;
        import java.lang.annotation.Target;
        
        @Retention(RetentionPolicy.RUNTIME)
        @Target(ElementType.METHOD)
        public @interface ClearContext {
        }
        

        并将其添加到 CleanupDatabaseTestExectionListener。

        @Override
        public void afterTestMethod(TestContext testContext) throws Exception {
            if(testContext.getTestMethod().getAnnotation(ClearContext.class)!=null){
                cleanupDatabase(testContext);
            }
            super.afterTestMethod(testContext);
        }
        

        在这两个 sn-ps 的帮助下,我现在可以创建如下测试:

        @Test
        @ClearContext
        public void testWhichDirtiesDatabase() {}
        

        【讨论】:

        • 如果我想使用内置库,我应该从上下文中获取哪个 bean?我有一个非常简单的应用程序,我不需要 Liquibase 或 Flyway。
        • 是清除数据库表还是重新创建整个sql方案?
        • 谢谢!我需要使用 afterTestMethod 而不是 afterTestClass 在每次测试执行之间清理数据库
        【解决方案6】:

        您可以在 org.springframework.transaction.annotation.Transactional 的 Junit 类级别使用 @Transactional 注释。

        例如:

        package org.test
        import org.springframework.transaction.annotation.Transactional;
        
        @Transactional
        public class ArmyTest{
        
        }
        

        【讨论】:

        • OP 说:“没有使整个测试成为事务”
        【解决方案7】:

        创建一个@Before 方法,在该方法中删除数据库中的所有数据。您正在使用 Hibernate,因此您可以使用 HQL:delete from Contract。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2019-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2013-11-08
          • 2018-07-21
          • 2019-08-29
          • 1970-01-01
          相关资源
          最近更新 更多