弹簧测试对MariadB数据库进行的回滚更改无@Transactional

发布于 2025-02-05 07:12:39 字数 1524 浏览 4 评论 0原文

我有一个这样做的春季服务:

@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
    }

}

传播。不表明我们在调用该方法时不得进行主动交易,因为我们不想在等待外部服务的答案时阻止与数据库的连接。

现在,我如何正确测试它,然后回滚数据库? @transactional在测试中无法使用,由于传播为止,会有一个例外。

@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创建的

当应用程序上下文启动我播放的其他想法

  • : @dirtiesContext:这要慢得多,创建一个新的上下文是更昂贵的不仅仅是在我的数据库中截断所有表中的所有表
  • :死胡同,这只是一种返回交易中数据库状态的方法。 这将是理想的解决方案IMO,
  • 如果我可以在全球范围内尝试使用连接, 请在测试前在数据源上发表启动交易语句,并且测试后rollback。肮脏,无法正常工作

I have a Spring service that does something like that :

@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
    }

}

The Propagation.NEVER indicates we must not have an active transaction when the method is called because we don't want to block a connection to the database while waiting for an answer from the external service.

Now, how could I properly test this and then rollback the database ? @Transactional on the test won't work, there will be an exception because of 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();
    }

}

I can remove the @Transactional but then my database is not in a consistent state for the next test.

For now my solution is to truncate all tables of my database after each test in a @AfterEach junit callback, but this is a bit clunky and gets quite slow when the database has more than a few tables.

Here comes my question : how could I rollback the changes done to my database without truncating/using @Transactional ?

The database I'm testing against is mariadb with testcontainers, so a solution that would work only with mariadb/mysql would be enough for me. But something more general would be great !

(another exemple where I would like to be able to not use @Transactional on the test : sometimes I want to test that transaction boundaries are correctly put in the code, and not hit some lazy loading exceptions at runtime because I forgot a @Transactional somewhere in the production code).

Some other precisions, if that helps :

  • I use JPA with Hibernate
  • The database is create with liquibase when the application context starts

Others ideas I've played with :

  • @DirtiesContext : this is a lot slower, creating a new context is a lot more expensive than just truncating all tables in my database
  • MariaDB SAVEPOINT : dead end, it's just a way to go back to a state of the database INSIDE a transaction. This would be the ideal solution IMO if i could work globally
  • Trying to fiddle with connections, issuing START TRANSACTION statements natively on the datasource before the test and ROLLBACK after the tests : really dirty, could not make it work

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

冬天旳寂寞 2025-02-12 07:12:40

这是一个疯狂的想法,但是如果您使用的是mySQL数据库,则可以切换到 dolt 进行测试?

dolt是一个SQL数据库

您可以将其包装为 testContainers容器,一开始就开始加载必要的数据每个测试运行 dolt reset

This is bit of wild idea, but if you are using mysql database, then maybe switch to dolt for tests?

Dolt is a SQL database that you can fork, clone, branch, merge, push and pull just like a git repository.

You can wrap it as testcontainers container, load necessary data on start and then, on start of each test run dolt reset.

赏烟花じ飞满天 2025-02-12 07:12:39

个人意见:@transactional + @springboottest is(以某种方式)与spring.jpa.jpa.open-in-in-in-cive 。是的,首先让事情运行很容易,并且自动回滚很不错,但是它使您对交易的灵活性和控制权很失去。任何需要手动交易管理的东西都很难以这种方式进行测试。

我们最近有一个非常相似的情况,最后我们决定咬住子弹并改用@dirtiesContext。是的,测试还需要30分钟的时间才能进行,但是作为额外的好处,测试服务的行为与生产中完全相同,并且测试更有可能捕获任何交易问题。

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

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

    void runWithoutTransaction(Runnable runnable);

}
@Service
public class RealTransactionService implements TransactionService
{

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

}
  1. 在您的其他服务中,用#runwithouttransaction -method包装外部HTTP调用,例如:
@Service
public class MyService
{
    @Autowired
    private TransactionService transactionService;

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

这样,您的生产代码将在paspagation.never检查中,并且对于测试,您可以使用没有其他没有@的实现来替换transactionservice交易注释,例如:

@Service
@Primary
public class FakeTransactionService implements TransactionService
{

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

}

这不仅限于opagation.never。可以以相同的方式实现其他传播类型:

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

最后 - 可以用函数/消费者/code>/替换参数参数。供应商如果该方法需要返回和/或接受值。

Personal opinion: @Transactional + @SpringBootTest is (in a way) the same anti-pattern as spring.jpa.open-in-view. Yes, it's easy to get things working at first and having the automatic rollback is nice, but it loses you a lot of flexibility and control over your transactions. Anything that requires manual transaction management becomes very hard to test that way.

We recently had a very similar case and in the end we decided to bite the bullet and use @DirtiesContext instead. Yeah, tests take 30 more minutes to run, but as an added benefit the tested services behave the exact same way as in production and the tests are more likely to catch any transaction issues.

But before we did the switch, we considered using the following workaround:

  1. Create an interface and a service similar to the following:
interface TransactionService
{

    void runWithoutTransaction(Runnable runnable);

}
@Service
public class RealTransactionService implements TransactionService
{

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

}
  1. In your other service wrap the external http calls with the #runWithoutTransaction-Method, e.g.:
@Service
public class MyService
{
    @Autowired
    private TransactionService transactionService;

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

That way your production code will peform the Propagation.NEVER check, and for the tests you can replace the TransactionService with a different implemention that doesn't have the @Transactional annotations, e.g.:

@Service
@Primary
public class FakeTransactionService implements TransactionService
{

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

}

This is not limited to Propagation.NEVER. Other propagation types can be implemented in the same way:

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

And finally - the Runnable parameter can be replaced with a Function/Consumer/Supplier if the method needs to return and/or accept a value.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文