簡體   English   中英

單元測試數據庫驅動的應用程序的最佳策略是什么?

[英]What's the best strategy for unit-testing database-driven applications?

我使用很多Web應用程序,這些應用程序由后端不同復雜程度的數據庫驅動。 通常,存在與業務和表示邏輯分離的ORM層。 這使得對業務邏輯的單元測試相當簡單; 事物可以在離散模塊中實現,測試所需的任何數據都可以通過對象模擬來偽造。

但是測試ORM和數據庫本身一直充滿了問題和妥協。

多年來,我嘗試了一些策略,其中沒有一個完全滿足我。

  • 使用已知數據加載測試數據庫。 針對ORM運行測試並確認正確的數據返回。 這里的缺點是您的測試數據庫必須跟上應用程序數據庫中的任何模式更改,並且可能會不同步。 它還依賴於人工數據,並且可能不會暴露由於愚蠢的用戶輸入而發生的錯誤。 最后,如果測試數據庫很小,它將不會顯示缺失索引等低效率。 (好吧,最后一個不是真的應該使用單元測試,但它不會受到傷害。)

  • 加載生產數據庫的副本並對其進行測試。 這里的問題是你可能不知道在任何給定時間生產數據庫中有什么; 如果數據隨時間變化,您的測試可能需要重寫。

有些人指出,這兩種策略都依賴於特定的數據,單元測試應該只測試功能。 為此,我見過建議:

  • 使用模擬數據庫服務器,並僅檢查ORM是否正在發送正確的查詢以響應給定的方法調用。

您使用了哪些策略來測試數據庫驅動的應用程序? 什么對你有用?

我實際上已經使用了你的第一種方法取得了相當大的成功,但我認為這種解決方案可以解決一些問題:

  1. 保留整個架構和腳本,以便在源代碼管理中創建它,以便任何人都可以在簽出后創建當前數據庫架構。 此外,將樣本數據保存在部分構建過程中加載的數據文件中。 當您發現導致錯誤的數據時,請將其添加到示例數據中以檢查錯誤是否重新出現。

  2. 使用持續集成服務器構建數據庫模式,加載示例數據並運行測試。 這就是我們如何使測試數據庫保持同步(在每次測試運行時重建它)。 雖然這要求CI服務器具有對其自己的專用數據庫實例的訪問權和所有權,但我說每天建立3次我們的數據庫模式可以極大地幫助找到在交付之前可能找不到的錯誤(如果不是以后的話) )。 我不能說我在每次提交之前重建架構。 有人嗎? 通過這種方法你不必(也許我們應該,但如果有人忘記,這不是什么大問題)。

  3. 對於我的組,用戶輸入在應用程序級別(而不是db)完成,因此通過標准單元測試進行測試。

加載生產數據庫副本:
這是我上一份工作中使用的方法。 這是幾個問題的巨大痛苦原因:

  1. 副本將從生產版本中過時
  2. 將對副本的架構進行更改,並且不會傳播到生產系統。 在這一點上,我們有不同的模式。 不好玩。

模擬數據庫服務器:
我們目前的工作也是這樣做的。 在每次提交之后,我們對注入了模擬db訪問器的應用程序代碼執行單元測試。 然后我們每天三次執行上面描述的完整db構建。 我絕對推薦這兩種方法。

由於以下原因,我總是針對內存數據庫(HSQLDB或Derby)運行測試:

  • 它使您可以考慮在測試數據庫中保留哪些數據以及原因。 將生產數據庫運送到測試系統只是“我不知道我在做什么或為什么,如果有什么東西壞了,那不是我!” ;)
  • 它確保可以在新的位置輕松地重新創建數據庫(例如,當我們需要從生產中復制錯誤時)
  • 它極大地幫助了DDL文件的質量。

一旦測試開始,內存數據庫就會加載新數據,在大多數測試之后,我調用ROLLBACK來保持穩定。 始終保持測試數據庫中的數據穩定! 如果數據一直在變化,則無法進行測試。

數據從SQL,模板DB或轉儲/備份加載。 如果它們是可讀格式,我更喜歡轉儲,因為我可以將它們放在VCS中。 如果這不起作用,我使用CSV文件或XML。 如果我必須加載大量數據......我沒有。 您永遠不必加載大量數據:)不適用於單元測試。 性能測試是另一個問題,適用不同的規則。

我一直在問這個問題很長一段時間,但我認為沒有靈丹妙葯。

我目前所做的是模擬DAO對象並保持內存表示一個良好的對象集合,這些對象代表可以存在於數據庫中的有趣數據案例。

我用這種方法看到的主要問題是,你只覆蓋與DAO層交互的代碼,但從不測試DAO本身,根據我的經驗,我發現在該層上也發生了很多錯誤。 我還保留了一些針對數據庫運行的單元測試(為了在本地使用TDD或快速測試),但這些測試永遠不會在我的持續集成服務器上運行,因為我們沒有為此目的保留數據庫而且我認為在CI服務器上運行的測試應該是自包含的。

我覺得非常有趣的另一種方法,但並不總是值得花費一點時間,就是在單元測試中運行的嵌入式數據庫上創建用於生產的相同模式。

即使毫無疑問,這種方法可以提高您的覆蓋率,但也有一些缺點,因為您必須盡可能接近ANSI SQL,以使其適用於您當前的DBMS和嵌入式替換。

無論你認為什么與你的代碼更相關,有一些項目可以使它更容易,如DbUnit

即使有一些工具允許你以某種方式模擬你的數據庫(例如jOOQMockConnection ,這可以在這個答案中看到 - 免責聲明,我為jOOQ的供應商工作),我建議不要模擬更大的數據庫復雜的查詢。

即使您只是想集成測試您的ORM,請注意ORM向您的數據庫發出一系列非常復雜的查詢,這些查詢可能會有所不同。

  • 句法
  • 復雜
  • 訂單(!)

模擬所有這些以生成合理的虛擬數據是非常困難的,除非您實際在mock中構建一個小數據庫,它解釋傳輸的SQL語句。 話雖如此,使用一個眾所周知的集成測試數據庫,您可以使用眾所周知的數據輕松重置,您可以使用它來運行集成測試。

我使用第一個(對測試數據庫運行代碼)。 我看到你用這種方法提出的唯一實質性問題是模式失去同步的可能性,我通過在我的數據庫中保留版本號並通過應用每個版本增量的更改的腳本進行所有模式更改來處理。

我還首先針對我的測試環境進行所有更改(包括數據庫模式),因此它最終成為另一種方式:在所有測試通過后,將模式更新應用於生產主機。 我還在我的開發系統上保留了一對單獨的測試與應用程序數據庫,這樣我就可以在觸摸真實生產框之前驗證數據庫升級是否正常工作。

我正在使用第一種方法,但有點不同,可以解決您提到的問題。

運行DAO測試所需的一切都在源代碼管理中。 它包括用於創建數據庫的模式和腳本(docker非常適合這種情況)。 如果可以使用嵌入式DB - 我用它來提高速度。

與其他描述的方法的重要區別在於,測試所需的數據不是從SQL腳本或XML文件加載的。 除了一些有效常量的字典數據外,所有內容都是由應用程序使用實用程序函數/類創建的。

主要目的是使測試使用的數據

  1. 非常接近測試
  2. 顯式(使用SQL文件獲取數據使得查看哪些數據被哪個測試使用很成問題)
  3. 將測試與無關的變化隔離開來。

它基本上意味着這些實用程序允許在測試本身中以聲明方式僅指定測試所必需的內容,並省略不相關的內容。

為了讓這意味着什么在實踐中的一些想法,可以考慮對一些DAO與工程測試Comment s到Post S按寫Authors 為了測試這種DAO的CRUD操作,應該在DB中創建一些數據。 測試看起來像:

@Test
public void savedCommentCanBeRead() {
    // Builder is needed to declaratively specify the entity with all attributes relevant
    // for this specific test
    // Missing attributes are generated with reasonable values
    // factory's responsibility is to create entity (and all entities required by it
    //  in our example Author) in the DB
    Post post = factory.create(PostBuilder.post());

    Comment comment = CommentBuilder.comment().forPost(post).build();

    sut.save(comment);

    Comment savedComment = sut.get(comment.getId());

    // this checks fields that are directly stored
    assertThat(saveComment, fieldwiseEqualTo(comment));
    // if there are some fields that are generated during save check them separately
    assertThat(saveComment.getGeneratedField(), equalTo(expectedValue));        
}

與SQL腳本或帶有測試數據的XML文件相比,這有幾個優點:

  1. 維護代碼要容易得多(例如,在許多測試中引用的某些實體中添加強制列,例如Author,不需要更改大量文件/記錄,只需更改構建器和/或工廠)
  2. 特定測試所需的數據在測試本身中描述,而不是在其他文件中描述。 這種接近度對於測試可理解性非常重要。

回滾與提交

我發現測試在執行時會提交更方便。 首先,如果提交從未發生,則無法檢查某些效果(例如DEFERRED CONSTRAINTS )。 其次,當測試失敗時,可以在DB中檢查數據,因為它不會被回滾恢復。

因此,這有一個缺點,即測試可能會產生損壞的數據,這將導致其他測試失敗。 為了解決這個問題,我嘗試隔離測試。 在上面的示例中,每個測試都可以創建新的Author並且創建與其相關的所有其他實體,因此很少發生沖突。 為了處理可能被破壞但不能表示為DB級別約束的剩余不變量,我使用一些編程檢查可以在每次單個測試之后運行的錯誤條件(並且它們在CI中運行但通常在本地關閉以獲得性能原因)。

對於基於JDBC的項目(直接或間接,例如JPA,EJB,...),您可以模擬不是整個數據庫(在這種情況下,最好在真正的RDBMS上使用測試數據庫),但只能在JDBC級別進行模型化。

優勢是這樣的抽象,因為JDBC數據(結果集,更新計數,警告......)與后端無關:您的prod db,測試數據庫,或者只是為每個測試提供的一些模型數據案件。

通過針對每種情況模擬的JDBC連接,無需管理測試數據庫(清理,只需要一次測試,重新加載固件,......)。 每個模型連接都是隔離的,無需清理。 每個測試用例中只提供最少的必需夾具來模擬JDBC交換,這有助於避免管理整個測試數據庫的復雜性。

Acolyte是我的框架,其中包括用於此類模型的JDBC驅動程序和實用程序: http//acolyte.eu.org

暫無
暫無

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

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