通过不带@Transactional的 Spring 测试回滚对MariaDB数据库所做的更改

hgqdbh6s  于 2022-11-08  发布在  Spring
关注(0)|答案(2)|浏览(151)

我有一个Spring服务,它可以做类似这样的事情:

@Service
public class MyService {

    @Transactional(propagation = Propagation.NEVER)
    public void doStuff(UUID id) {
        // call an external service, via http for example, can be long
        // update the database, with a transactionTemplate for example
    }

}

Propagation.NEVER表示在调用该方法时不能有活动事务,因为我们不希望在等待外部服务的应答时阻塞到数据库的连接。
现在,我该如何正确地测试它,然后回滚数据库呢?@测试中的Transactional将不起作用,因为Propagation. NEVER而将出现异常。

@SpringBootTest
@Transactional
public class MyServiceTest {

    @Autowired
    private MyService myService;

    public void testDoStuff() {
       putMyTestDataInDb();
       myService.doStuff();    // <- fails no transaction should be active
       assertThat(myData).isTheWayIExpectedItToBe();
    }

}

我可以删除@Transactional,但这样我的数据库将无法在下一次测试中处于一致状态。
目前,我的解决方案是在@AfterEach junit回调中的每次测试后截断数据库的所有表,但这有点笨拙,并且当数据库有多个表时会变得相当慢。

我的问题来了:如何在不截断/使用@Transactional的情况下回滚对数据库所做的更改?

我正在测试的数据库是mariadb和testcontainers,所以一个只适用于mariadb/mysql的解决方案对我来说已经足够了,但是更通用的解决方案会更好!
(另一个我希望在测试中不使用@Transactional的示例:有时我想测试事务边界是否正确地放在代码中,而不是在运行时遇到一些延迟加载异常,因为我在生产代码中的某个地方忘记了@Transactional。
如果有帮助的话,还有一些其他的精确性:

  • 我将JPA与Hibernate结合使用
  • 当应用程序上下文启动时,使用liquibase创建数据库

其他想法我玩过:

  • @脏上下文:这要慢得多,创建一个新的上下文比仅仅截断数据库中的所有表要昂贵得多
  • MariaDB保存点:死胡同,这只是一种方式回到数据库的状态内的事务。这将是理想的解决方案IMO如果我可以全球工作
  • 尝试摆弄连接,在测试之前在数据源上本地发出START TRANSACTION语句,在测试之后发出ROLLBACK语句:很脏,无法使用
mkh04yzy

mkh04yzy1#

个人意见:@Transactional + @SpringBootTest(在某种程度上)与spring.jpa.open-in-view是相同的反模式。是的,一开始很容易让事情正常工作,自动回滚也很好,但是它会让你失去很多灵活性和对事务的控制。任何需要手动事务管理的东西都很难用这种方式进行测试。
我们最近遇到了一个非常类似的案例,最后我们决定咬紧牙关,改用@DirtiesContext。是的,测试需要多花30分钟来运行,但作为一个额外的好处,被测试的服务的行为方式与生产中完全相同,测试更有可能捕捉到任何事务问题。
但在进行切换之前,我们考虑使用以下解决方法:
1.创建一个类似于以下内容的接口和服务:
第一个
1.在您的其他服务中,使用#runWithoutTransaction-方法 Package 外部http调用,例如:

@Service
public class MyService
{
    @Autowired
    private TransactionService transactionService;

    public void doStuff(UUID id)
    {
        transactionService.runWithoutTransaction(() -> {
            // call an external service
        })
    }
}

这样,您的产品代码将执行Propagation.NEVER检查,并且对于测试,您可以将TransactionService替换为不具有@Transactional注解的不同实现,例如:

@Service
@Primary
public class FakeTransactionService implements TransactionService
{

    // No annotation here
    public void runWithoutTransaction(Runnable runnable)
    {
        runnable.run();
    }

}

这并不限于Propagation.NEVER。其他传播类型也可以用相同的方式实现:

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void runWithNewTransaction(Runnable runnable)
{
    runnable.run();
}

最后,如果方法需要返回和/或接受一个值,则Runnable参数可以替换为Function/Consumer/Supplier

b5lpy0ml

b5lpy0ml2#

这是一个有点疯狂的想法,但如果你正在使用mysql数据库,那么也许切换到dolt进行测试?
Dolt是一个SQL数据库,你可以像git仓库一样分叉、克隆、分支、合并、推送和拉取。
您可以将其 Package 为testcontainers container,在启动时加载必要的数据,然后在每个测试运行开始时dolt重置。

相关问题