如何在每次单元测试后重置数据库状态而不将整个测试作为事务处理?

时间:2013-01-15 18:00:51

标签: spring junit transactions hsqldb

我正在使用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”),每次调用都被视为一个事务,我想要它。但我不知道在每次单元测试后如何将内存数据库重置为原始状态。

如果我需要提供其他信息,请与我们联系。

7 个答案:

答案 0 :(得分:18)

由于您正在使用hibernate,因此您可以使用属性hibernate.hbm2ddl.auto每次启动时创建数据库。您还需要在每次测试后强制重新加载spring上下文。您可以使用@DirtiesContext注释执行此操作。

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

答案 1 :(得分:7)

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

@Before对我来说也不是一个好的解决方案,因为我必须在每个IntegrationTest中创建@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 {
 }
编辑:我改进了我的解决方案。我遇到的问题是,有时一个测试方法会破坏我的数据库,以便测试类中所有即将进行的测试。所以我创建了注释

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

在这两个片段的帮助下,我现在可以创建这样的测试:

@Test
@ClearContext
public void testWhichDirtiesDatabase() {}

答案 2 :(得分:2)

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

例如:

package org.test
import org.springframework.transaction.annotation.Transactional;

@Transactional
public class ArmyTest{

}

答案 3 :(得分:1)

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

答案 4 :(得分: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.

答案 5 :(得分:0)

对于每个测试,我都使用随机内存数据库解决了相同的问题:

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

答案 6 :(得分:0)

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

public class RepositoryFactory
{
    public static TRepository GetRepositoryInstance<T, TRepository>()
          where TRepository : IRepository<T>, new()
          where T : class, IEntity
    {
        return new TRepository();
    }
}

result=[] for (i, r1), (j, r2) in itertools.combinations(df.iterrows(), 2): if r1['SERVICE CODE'] != r2['SERVICE CODE']: result.append('{}_{}'.format(r1['PORT CODE'], r2['PORT CODE'])) dff=pd.DataFrame(result) 使您可以使import os import numpy as np from concurrent.futures import ProcessPoolExecutor def f(x, y): print(os.getpid(), x, y, x + y) return x + y if __name__ == '__main__': xs = np.linspace(5, 7, 3).astype(int) ys = np.linspace(1, 3, 3).astype(int) with ProcessPoolExecutor() as executor: results = executor.map(f, xs, ys) for res in results: print(res) 为非静态,因此每个测试类只能迁移一次。如果要为每个测试重置它,请删除类注释,并将@TestInstance(TestInstance.Lifecycle.PER_CLASS) class JUnit5Class { @Autowired Flyway flyway; @BeforeAll public void cleanUp(){ flyway.clean(); flyway.migrate(); } } 更改为@TestInstance