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

Séb*_*mer 5 java junit spring transactions mariadb

我有一个 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
    }

}
Run Code Online (Sandbox Code Playgroud)

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

}
Run Code Online (Sandbox Code Playgroud)

我可以删除@Transactional,但是我的数据库对于下一个测试来说并没有处于一致的状态。

目前,我的解决方案是在 @AfterEach junit 回调中的每次测试后截断数据库的所有表,但这有点笨拙,并且当数据库有多个表时会变得相当慢。

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

我正在测试的数据库是带有 testcontainers 的 mariadb,因此仅适用于 mariadb/mysql 的解决方案对我来说就足够了。但更通用的东西会很棒!

(另一个例子,我希望能够在测试中不使用@Transactional:有时我想测试事务边界是否正确放入代码中,并且在运行时不会遇到一些延迟加载异常,因为我在某处忘记了@Transactional在生产代码中)。

其他一些精度,如果有帮助的话:

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

我玩过的其他想法:

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

vto*_*osh 2

个人观点:@Transactional+ @SpringBootTest(在某种程度上)与 相同的反模式spring.jpa.open-in-view。是的,一开始很容易让事情正常运行,并且自动回滚很好,但它会失去很多灵活性和对事务的控制。任何需要手动事务管理的事情都很难以这种方式进行测试。

我们最近遇到了一个非常相似的案例,最后我们决定硬着头皮使用@DirtiesContext。是的,测试还需要 30 分钟才能运行,但作为一个额外的好处,测试服务的行为方式与生产中的完全相同,并且测试更有可能捕获任何事务问题。

但在进行切换之前,我们考虑使用以下解决方法:

  1. 创建类似于以下的接口和服务:
interface TransactionService
{

    void runWithoutTransaction(Runnable runnable);

}
Run Code Online (Sandbox Code Playgroud)
@Service
public class RealTransactionService implements TransactionService
{

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

}
Run Code Online (Sandbox Code Playgroud)
  1. 在您的其他服务中,使用 -Method 包装外部 http 调用#runWithoutTransaction,例如:
@Service
public class MyService
{
    @Autowired
    private TransactionService transactionService;

    public void doStuff(UUID id)
    {
        transactionService.runWithoutTransaction(() -> {
            // call an external service
        })
    }
}
Run Code Online (Sandbox Code Playgroud)

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

@Service
@Primary
public class FakeTransactionService implements TransactionService
{

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

}
Run Code Online (Sandbox Code Playgroud)

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

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void runWithNewTransaction(Runnable runnable)
{
    runnable.run();
}
Run Code Online (Sandbox Code Playgroud)

最后 -如果方法需要返回和/或接受一个值,则可以用//Runnable替换参数。FunctionConsumerSupplier