簡體   English   中英

通過沒有 @Transactional 的彈簧測試回滾對 MariaDB 數據庫所做的更改

[英]Rollback changes done to a MariaDB database by a spring test without @Transactional

我有一個執行類似操作的 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 的情況下回滾對數據庫所做的更改?

我正在測試的數據庫是帶有 testcontainers 的 mariadb,因此僅適用於 mariadb/mysql 的解決方案對我來說就足夠了。 但是更一般的東西會很棒!

(另一個我希望不能在測試中使用 @Transactional 的示例:有時我想測試事務邊界是否正確放入代碼中,並且不會在運行時遇到一些延遲加載異常,因為我在某處忘記了 @Transactional在生產代碼中)。

其他一些精度,如果有幫助的話:

  • 我將 JPA 與 Hibernate 一起使用
  • 應用程序上下文啟動時使用 liquibase 創建數據庫

我玩過的其他想法:

  • @DirtiesContext :這要慢得多,創建新上下文比截斷數據庫中的所有表要昂貴得多
  • MariaDB SAVEPOINT :死路一條,這只是一種回到事務內部數據庫狀態的方法。 如果我可以在全球范圍內工作,這將是 IMO 的理想解決方案
  • 試圖擺弄連接,在測試之前在數據源上本地發出START TRANSACTION語句,在測試之后發出ROLLBACK :真的很臟,無法讓它工作

個人觀點: @Transactional + @SpringBootTest (在某種程度上)與spring.jpa.open-in-view相同的反模式。 是的,一開始很容易讓事情正常工作,並且自動回滾很好,但它會失去很多靈活性和對事務的控制。 任何需要手動事務管理的東西都很難以這種方式進行測試。

我們最近有一個非常相似的案例,最后我們決定硬着頭皮改用@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包裝外部 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();
}

最后 - 如果方法需要返回和/或接受一個值,則可以用Function / Consumer / Supplier替換Runnable參數。

這有點瘋狂,但是如果您使用的是 mysql 數據庫,那么也許切換到dolt進行測試?

Dolt 是一個 SQL 數據庫,您可以像 git 存儲庫一樣分叉、克隆、分支、合並、推送和拉取。

您可以將其包裝為testcontainers 容器,在啟動時加載必要的數據,然后在每次測試運行開始時加載 dolt reset

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM