Spring陷阱:事務測試被認為是有害的

Spring殺手級功能之一是容器內集成測試 。 盡管EJB多年來一直缺乏此功能(Java EE 6終于解決了這個問題,但是我還沒測試過),但是Spring從一開始就允許您從Web層開始,通過所有服務來測試整個堆棧。到數據庫的方式。

數據庫是有問題的部分。 首先,您需要使用內存中的獨立數據庫(例如H2)將測試與外部數據庫分離。 Spring在很大程度上幫助了這一點,尤其是現在有了配置文件和嵌入式數據庫支持 。 第二個問題更加微妙。 雖然典型的Spring應用程序幾乎是完全無狀態的(無論好壞),但數據庫固有地是有狀態的。 這使集成測試變得復雜,因為編寫測試的第一個原則是它們應該彼此獨立且可重復。 如果一個測試將某些內容寫入數據庫,則另一個測試可能會失敗; 而且由于數據庫更改,相同的測試可能在后續調用中失敗。

顯然,Spring還可以通過一個非常巧妙的技巧來處理此問題 :在運行每個測試之前,Spring啟動一個新事務。 整個測試(包括其設置和拆除)在同一事務中運行,該事務在最后回滾。 這意味著測試期間所做的所有更改在數據庫中都是可見的,就像它們是持久的一樣。 但是,每次測試后的回滾將清除所有更改,并且下一個測試將在全新的數據庫上進行。 輝煌!

不幸的是,這不是關于Spring集成測試優勢的另一篇文章。 我想我已經編寫了成百上千個這樣的測試,而我真的很感謝Spring框架提供的透明支持。 但是我也遇到了這個舒適功能帶來的眾多怪異和不一致之處。 更糟的是,通常所謂的事務測試實際上隱藏了錯誤,使開發人員確信該軟件可以工作,而部署后卻會失敗! 這是一個不盡詳盡但令人大開眼界的問題集合:

@Test
public void shouldThrowLazyInitializationExceptionWhenFetchingLazyOneToManyRelationship() throws Exception {//givenfinal Book someBook = findAnyExistingBook();//whentry {someBook.reviews().size();fail();} catch(LazyInitializationException e) {//then}
}

這是Hibernate和spring集成測試中的一個已知問題。 Book是一個數據庫實體,與“評論”具有一對多的關系,默認情況下是惰性的。 findAnyExistingBook()只是從事務服務中讀取測試書。 現在有一點理論:只要將實體綁定到會話(如果使用JPA,則為EntityManager),它就可以延遲和透明地加載關系。 對我們而言,這意味著:只要它在交易范圍之內。 實體離開交易的那一刻,它就變得分離。 在此生命周期階段,實體不再連接到session / EntityManager(已經提交并關閉),并且任何獲取懶惰屬性的方法都將引發可怕的LazyInitializationException。 此行為實際上是在JPA中標準化的(異常類本身除外,后者是特定于供應商的)。

在本例中,我們正在調用.reviews()(Scala風格的“ getter”,我們也將盡快將測試用例轉換為ScalaTest),并期望看到Hibernate異常。 但是不會引發異常,并且應用程序繼續運行。 這是因為整個測試都在事務內運行,并且Book實體永遠不會超出事務范圍。 延遲加載在Spring集成測試中始終有效。

公平地說,我們在現實生活中永遠不會看到這樣的測試(除非您要進行測試以確保給定的集合是惰性的-不太可能)。 在現實生活中,我們正在測試僅在測試中起作用的業務邏輯。 但是,在部署之后,我們開始遇到LazyInitializationException。 但是我們測試了! Spring集成測試支持不僅隱藏了該問題 ,而且還鼓勵開發人員使用OpenSessionInViewFilter或OpenEntityManagerInViewFilter 。 換句話說:我們的測試不僅沒有發現代碼中的錯誤,而且還大大惡化了我們的整體體系結構和性能。 不是我所期望的。

目前,實現某些端到端功能時,我通常的工作流程是編寫后端測試,實現包括REST API的后端,并在一切運行順利時進行部署并繼續使用GUI。 后者是完全使用AJAX / JavaScript編寫的,因此我只需要部署一次并經常替換便宜的客戶端文件。 在此階段,我不想回到服務器來修復未發現的錯誤。

抑制LazyInitializationException是Spring集成測試中最著名的問題之一。 但這只是冰山一角。 這是一個更復雜的示例(它再次使用JPA,但是此問題在普通JDBC和任何其他持久性技術中也很明顯):

@Test
public void externalThreadShouldSeeChangesMadeInMainThread() throws Exception {//givenfinal Book someBook = findAnyExistingBook();someBook.setAuthor("Bruce Wayne");bookService.save(someBook);//whenfinal Future<Book> future = executorService.submit(new Callable<Book>() {@Overridepublic Book call() throws Exception {return bookService.findBy(someBook.id()).get();}});//thenassertThat(future.get().getAuthor()).isEqualTo("Bruce Wayne");
}

第一步,我們從數據庫中加載一些書籍并修改作者,然后保存一個實體。 然后,我們通過id在另一個線程中加載相同的實體。 該實體已經保存,因此可以保證該線程可以看到更改。 但是,情況并非如此,最后一步中的斷言證明了這一點。 發生了什么?

我們剛剛在ACID事務屬性中觀察到“ I”。 在提交事務之前,測試線程所做的更改對其他線程/連接不可見。 但是我們知道測試事務已提交! 這個小展示展示了在事務支持下編寫多線程集成測試有多么困難。 幾周前,當我想對啟用JDBCJobStore的 Quartz調度程序進行集成測試時,我學到了很難的方法。 無論我多么努力,這些工作從未被解雇。 事實證明,我正在Spring事務范圍內安排在Spring托管測試中的工作。 由于從未提交過事務,因此外部調度程序和工作線程無法在數據庫中看到新的作業記錄。 您花了幾個小時調試此類問題?

在談論調試時,對數據庫相關的測試失敗進行故障排除時會彈出相同的問題。 我可以將此簡單的H2 Web控制臺(瀏覽到localhost:8082)bean添加到我的測試配置中:

@Bean(initMethod = "start", destroyMethod = "stop")
def h2WebServer() = Server.createWebServer("-webDaemon", "-webAllowOthers")

但是在逐步進行測試時,我將永遠不會看到測試所做的更改。 我無法手動運行查詢以查明為什么返回錯誤結果。 另外,在進行故障排除時,我無法即時修改數據以更快地周轉。 我的數據庫位于另一個維度。

請仔細閱讀下一個測試,時間不長:

@Test
public void shouldNotSaveAndLoadChangesMadeToNotManagedEntity() throws Exception {//givenfinal Book unManagedBook = findAnyExistingBook();unManagedBook.setAuthor("Clark Kent");//whenfinal Book loadedBook = bookService.findBy(unManagedBook.id()).get();//thenassertThat(loadedBook.getAuthor()).isNotEqualTo("Clark Kent");
}

我們正在加載一本書并修改作者, 而沒有明確地堅持下去。 然后,我們再次從數據庫中加載它,并確保更改未保留。 猜猜是什么,我們已經以某種方式更新了對象!

如果您是經驗豐富的JPA / Hibernate用戶,那么您將確切地知道如何發生。 還記得我在上面描述附著/分離的實體時的情況嗎? 當實體仍附加到基礎EntityManager /會話時,它也具有其他權力。 JPA提供者有義務跟蹤對此類實體所做的更改,并在實體分離時將其自動傳播到數據庫(所謂的臟檢查)。

這意味著使用JPA實體修改的慣用方式是從數據庫中加載對象,使用setter執行必要的更改,僅此而已。 當實體分離時,JPA將發現它已被修改并為您發出UPDATE。 不需要merge()/ update(),可愛的對象抽象。 只要管理實體,此方法就起作用。 對分離的實體所做的更改將被靜默忽略,因為JPA提供程序對此類實體一無所知。 現在最好的部分–您幾乎不知道您的實體是否已附加,因為事務管理是透明的并且幾乎是不可見的。 這意味著只修改內存中的POJO實例,同時仍然認為更改是持久的,反之亦然,這太容易了!

我們可以測試嗎? 當然,我們只是做了–失敗了。 在我們的測試中,交易涉及整個測試方法,因此每個實體都受到管理。 同樣由于Hibernate L1緩存,即使尚未發布數據庫更新,我們也可以獲取完全相同的圖書實例。 這是事務測試隱藏問題而不是揭示問題的另一種情況(請參閱LazyInitializationException示例)。 更改將如測試中所預期的那樣傳播到數據庫,但是在部署后將被靜默忽略……

順便說一句,我是否提到過,一旦您放棄了對測試用例類的@Transactional注釋,到目前為止所有測試都通過了? 看看,來源永遠是可用的 。

這是令人興奮的。 我有一個事務性的deleteAndThrow(book)業務方法,該方法刪除給定的書并引發OppsException。 這是我通過的測試,證明代碼正確:

@Test
public void shouldDeleteEntityAndThrowAnException() throws Exception {//givenfinal Book someBook = findAnyExistingBook();try {//whenbookService.deleteAndThrow(someBook);fail();} catch (OppsException e) {//thenfinal Option<Book> deletedBook = bookService.findBy(someBook.id());assertThat(deletedBook.isEmpty()).isTrue();}}

返回了Scala的Option <Book> (您是否已經注意到Java代碼與用Scala編寫的服務和實體交互得很好嗎?),而不是null。 如果deleteBook.isEmpty()的結果為true,則表示未找到結果。 因此,似乎我們的代碼是正確的:實體已刪除,并且引發了異常。 是的,您是正確的,它在再次部署后會靜默失敗! 這次,Hibernate L1緩存知道該特定的book實例已刪除,因此即使在刷新更改到數據庫之前,它也返回null。 但是,從服務拋出的OppsException會回滾事務,并丟棄DELETE! 但是測試通過了,只是因為Spring管理著這個微小的額外事務,并且斷言發生在該事務內。 毫秒后,事務回滾,恢復已刪除的實體。

顯然,解決方案是為OppsException添加noRollbackFor屬性(這是我在放棄事務性測試以支持其他解決方案后在代碼中發現的實際錯誤,目前尚待解釋)。 但這不是重點。 關鍵是– 您真的可以負擔起編寫和維護正在生成錯誤肯定結果的測試,說服您的應用程序正常運行的能力,而事實并非如此?

哦,我是否提到過跨語言測試實際上在這里和那里泄漏,并且不會阻止您修改測試數據庫? 第二次測試失敗,您知道為什么嗎?

@Test
public void shouldStoreReviewInSecondThread() throws Exception {final Book someBook = findAnyExistingBook();executorService.submit(new Callable<Review>() {@Overridepublic Review call() throws Exception {return reviewDao.save(new Review("Unicorn", "Excellent!!!1!", someBook));}}).get();
}
@Test
public void shouldNotSeeReviewStoredInPreviousTest() throws Exception {//given//whenfinal Iterable<Review> reviews = reviewDao.findAll();//thenassertThat(reviews).isEmpty();
}

線程再次陷入困境。 當您嘗試在顯然已提交的后臺線程中進行外部事務處理后進行清理時,它會變得更加有趣。 自然的地方是在@After方法中刪除創建的Review。 但是@After是在同一測試事務中執行的,因此清理將…回滾。

當然,我并不是在抱怨我最喜歡的應用程序堆棧弱點。 我在這里提供解決方案和提示。 我們的目標是完全擺脫事務測試,僅依賴于應用程序事務。 這將有助于我們避免上述所有問題。 顯然,我們不能放棄測試的獨立性和可重復性功能。 每個測試必須在同一數據庫上工作才能可靠。 首先,我們將把JUnit測試轉換為ScalaTest。 為了獲得Spring依賴注入支持,我們需要這個微小的特征:

trait SpringRule extends Suite with BeforeAndAfterAll { this: AbstractSuite =>override protected def beforeAll() {new TestContextManager(this.getClass).prepareTestInstance(this)super.beforeAll();}}

現在是時候揭示我的想法了(如果您不耐煩,請在此處查看完整的源代碼 )。 它遠非獨創性或獨創性,但我認為它值得關注。 無需在一個巨大的事務中運行所有內容并將其回滾,只需讓經過測試的代碼在需要和配置的任何地方,任何時間啟動和提交事務即可。 這意味著數據實際上已寫入數據庫,并且持久性與部署后的工作原理完全相同。 哪里有收獲? 每次測試后,我們都必須以某種方式清理混亂……

事實證明它并不那么復雜。 只需轉儲干凈的數據庫,然后在每次測試后將其導入! 轉儲包含在部署和應用程序啟動之后,第一次測試運行之前立即存在的所有表,約束和記錄。 就像備份并從中還原一樣! 看看H2有多簡單:

trait DbResetRule extends Suite with BeforeAndAfterEach with BeforeAndAfterAll { this: SpringRule =>@Resource val dataSource: DataSource = nullval dbScriptFile = File.createTempFile(classOf[DbResetRule].getSimpleName + "-", ".sql")override protected def beforeAll() {new JdbcTemplate(dataSource).execute("SCRIPT NOPASSWORDS DROP TO '" + dbScriptFile.getPath + "'")dbScriptFile.deleteOnExit()super.beforeAll()}override protected def afterEach() {super.afterEach()new JdbcTemplate(dataSource).execute("RUNSCRIPT FROM '" + dbScriptFile.getPath + "'")}}trait DbResetSpringRule extends DbResetRule with SpringRule

SQL轉儲(請參閱H2 SCRIPT命令)執行一次并導出到臨時文件。 然后在每次測試后執行SQL腳本文件。 信不信由你,就是這樣! 我們的測試不再是事務性的(因此,所有Hibernate和多線程的極端情況都已發現并測試了),而我們并沒有犧牲事務性測試設置的簡便性(不需要額外的清理)。 我最終還可以在調試時查看數據庫內容! 這是進行中的先前測試之一:

@RunWith(classOf[JUnitRunner])
@ContextConfiguration(classes = Array[Class[_]](classOf[SpringConfiguration]))
class BookServiceTest extends FunSuite with ShouldMatchers with BeforeAndAfterAll with DbResetSpringRule {@Resourceval bookService: BookService = nullprivate def findAnyExistingBook() = bookService.listBooks(new PageRequest(0, 1)).getContent.headtest("should delete entity and throw an exception") {val someBook = findAnyExistingBook()intercept[OppsException] {bookService.deleteAndThrow(someBook)}bookService findBy someBook.id should be (None)}
}

請記住,這不是一個庫/實用程序,而是一個想法。 對于您的項目,您可能會選擇略有不同的方法和工具,但是總體思路仍然適用:讓您的代碼在與部署后完全相同的環境中運行,然后從備份中清除混亂。 您可以使用JUnit, HSQLDB或任何您喜歡的方法獲得完全相同的結果。 當然,您還可以添加一些巧妙的優化方法-標記或發現未修改數據庫的測試,選擇更快的轉儲,導入方法等。

老實說,還有一些弊端,以下是我腦海中的一些缺點:

  • 性能 :盡管這種方法并不總是比回滾事務慢得多(某些數據庫回滾特別慢),但并不明顯,但可以肯定地說。 當然,內存數據庫可能具有一些意外的性能特征,但要為速度變慢做好準備。 但是,我沒有在一個小項目中觀察到每100個測試有巨大的差異(大約10%)。
  • 并發性 :您不再可以同時運行測試。 一個線程(測試)所做的更改對其他線程可見,從而使測試執行無法預測。 對于上述性能問題,這甚至變得更加痛苦。

就是這樣。 如果您有興趣,請給這種方法一個機會。 采用您現有的測試基礎可能需要一些時間,但是即使發現一個隱藏的bug也值得,您是不是認為呢? 并且還要注意其他Spring陷阱 。

參考: Spring陷阱: NoBlogDefFound博客上的 JCG合作伙伴 Tomasz Nurkiewicz 認為有害的事務測試 。

相關文章 :
  • Spring陷阱:代理
  • Spring聲明式事務示例
  • Spring依賴注入技術的發展
  • Spring和AspectJ的領域驅動設計
  • Spring 3使用JUnit 4進行測試– ContextConfiguration和AbstractTransactionalJUnit4SpringContextTests
  • 使用Spring AOP進行面向方面的編程
  • Java教程和Android教程列表

翻譯自: https://www.javacodegeeks.com/2011/12/spring-pitfalls-transactional-tests.html

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/374168.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/374168.shtml
英文地址,請注明出處:http://en.pswp.cn/news/374168.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

python xlwt寫入已有表_Python中,添加寫入數據到已經存在的Excel文件

1.安裝xlrd、xlwt、xlutilshttps://pypi.org/project/xlutils/pip安裝&#xff1a;cmd下輸入&#xff1a;pip install xlrd #讀取exclepip install xlwt #寫入exclepip install xlutils #操作 Excel 文件的實用工具&#xff0c;如復制、分割、篩選等2.代碼主要部分實現import x…

java線程“生產/消費”模型2

/* 資源類 */ class ShareValue {private int total;//判斷對象是否為空private boolean isEmptytrue;//判斷對象是否已滿private boolean isFulltrue;public ShareValue(int total) {this.total total;if(total>0) isEmptyfalse;if(total<1000) isFullfalse;}/** sync…

linux更改用戶名_破舊安卓手機第二春,在安卓手機上使用Linux_deploy運行Linux

由于服務器位于國外&#xff0c;害怕被墻掉導致數據丟失&#xff0c;所以在本地寫了腳本每小時從服務器上導出并下載到本地。但是電腦不可能二十四小時開機&#xff0c;所以很想買一個樹莓派4玩玩。但是太貴遼&#xff0c;還好搜索到了Android運行Linux的方法&#xff0c;下面記…

關于Cocos2d-x中init方法和onEnter方法的區別

init()和onEnter()這兩個方法都是寫實例化對象的類(比如繼承自Node的一些類等等)的時候用到的方法。 一般都是public類型下面的 bool init(); void onEnter(); 兩個方法在實現的時候都要先執行父類方法&#xff0c;比如 Node::init(); Node::onEnter(); 注意&#xff1a; 1.ini…

在5分鐘內在MacOSX Lion中設置JAVA_HOME,MAVEN_HOME,ANT_HOME

人們一直試圖通過Mac上的Java開發世界來解決這個問題&#xff0c;這一直是我一直遇到的問題。 他們必須解決的第一件事就是設置適當的工具和環境。 幸運的是&#xff0c;足夠多的MacOSX &#xff08;Lion或以前的版本&#xff09;仍然有許多重要的工具可用于Java開發&#xff0…

MCUXpress IDE常用設置

NXP的開發工具Xpress是基于eclipse制作的&#xff0c;我們如果需要設置一些東西可以直接搜索eclipse是怎么設置的。 1、字體大小 搜索eclipse字體大小&#xff0c;菜單Window > Preference 而Xpress是漢化了的&#xff0c;英語不好的同學可能懵逼&#xff0c;其實就是菜單欄…

C語言中空格符、空字符、字符數組結束符、換行、回車的區別

空格符和空字符是不一樣的,在ASCII里面,空格(space)符號的ASCII碼是32,而空字符是0, 2個是完全不一樣的2個字符 空字符 一般來描述一個字符串的結尾,其實是控制符的一種,但不能理解為沒有字符,應該理解為代表什么都沒有的字符.好比回車0x0A和換行0x0D雖然不顯示,但是也是控制字…

hdu5823 (附帶數的二進制子集)

二進制數子集的取法,結果不會輸出0&#xff0c;且從大到小 for(int i0 i;i0;i0(i0-1)&i)cout<<i0<<endl; 題意&#xff1a; 給定一個 N個點的圖&#xff0c; 求它的每一個子圖的最小染色數 染色方法是所有子圖中相連接兩點顏色不一致 其中 N≤18 題解&…

anaconda如何卸載庫_小白必看!Anaconda安裝全攻略

本文作者&#xff1a;戴 雯文字編輯&#xff1a;方 言技術總編&#xff1a;張馨月爬蟲俱樂部云端課程來襲&#xff01;爬蟲俱樂部將于2020年8月25日至28日在線上舉行Stata數據分析法律與制度專題訓練營&#xff0c;主要是為了讓學員掌握Stata軟件進階操作&#xff0c;涉及…

RESTful Web服務可發現性,第4部分

這是有關使用Spring 3.1和Spring Security 3.1和基于Java的配置來建立安全的RESTful Web Service的系列文章的第四篇 。 本文將重點介紹REST API&#xff0c;HATEOAS的可發現性以及由測試驅動的實際方案。 引入REST可發現性 API的可發現性是一個值得引起足夠關注的主題&#x…

10位IT領袖給應屆畢業生的10條忠告

10位IT領袖給應屆畢業生的10條忠告&#xff0c;在走向獨立和自主的偉大征程中&#xff0c;吸取他們的經驗。 在畢業生們邁出象牙塔之時&#xff0c;他們應該聽從哪些人的建議&#xff1f;在走向獨立和自主的偉大征程中&#xff0c;他們該吸取哪些教訓&#xff1f;聽一聽各領域…

ubuntu安裝好后常用軟件安裝和配置

1、安裝vim sudo apt install vim 安裝好后進入路徑打開vimrc文件&#xff0c;這里需要注意一定要用sudo不然編輯后無法保存&#xff01; cd /etc/vim sudo vim vimrc 在最下面加入 set nu set ts4 set softtabstop4 set shiftwidth4 set expandtab set autoindent 依次是…

Objective-c 數據類型

這里列出Objective-c中獨有數據類型&#xff1a; 一、字符串 在Objective-c中&#xff0c;字符串常量是由和一對從引號括起的字符串序列。比如&#xff1a;"China"、"objective-c"等都是合法的字符串常量。 二、id類型 id類型是Objective-c中一個比較獨…

JBoss AS 7 EJB3池配置

現在&#xff0c;AS 7.0.1已經發布&#xff0c;讓我們看一下可用的EJB3新功能。 就像我在上一篇文章中提到的那樣 &#xff0c;AS 7.0.1現在允許您為無狀態會話bean和MDB配置池。 當前&#xff0c;我們允許在子系統級別配置池&#xff0c;這意味著該池將適用于服務器上部署的所…

iOS開發網絡篇—文件的上傳

說明&#xff1a;文件上傳使用的時POST請求&#xff0c;通常把要上傳的數據保存在請求體中。本文介紹如何不借助第三方框架實現iOS開發中得文件上傳。 由于過程較為復雜&#xff0c;因此本文只貼出部分關鍵代碼。 主控制器的關鍵代碼&#xff1a; YYViewController.m 1 #import…

var模型的matlab實現_Eviews中VAR模型的操作、脈沖響應分析和方差分解的實現

打開文件所在位置&#xff0c;獲取數據。選中變量右鍵open打開var操作EViews,在VAR對象的工具欄中選擇“View”|“Lag Structure”|“AR Roots Table/ AR Roots Graph”選項&#xff0c;得到AR根的表和圖。結果顯示&#xff1a;VAR模型所有根模的倒數都小于1&#xff0c;即都在…

一個程序員的愛情表白書

我能抽象出整個世界 但是我不能抽象出你 因為你在我心中是那么的具體 所以我的世界并不完整 我可以重載甚至覆蓋這個世界里的任何一種方法 但是我卻不能重載對你的思念 也許命中注定了 你在我的世界里永遠的烙上了靜態的屬性 而我不慎調用了愛你這個方法 當我義無返顧的…

結構體、枚舉類型

一、結構體 結構體&#xff1a;就是一個自定義的集合&#xff0c;里面可以放各種類型的元素&#xff0c;用法大體跟集合一樣。 1、定義的方法&#xff1a; struct student { public int nianling; public int fenshu; public string name; public string sex; public int sum; …

NXP KW38開發雜記(一)MCUXpress 運行進入NMI_Handler

這里是大佬的具體分析過程&#xff0c;感興趣可以看看 https://www.cnblogs.com/wenhao-Web/p/13618703.html 解決辦法&#xff1a; 在startup_mkw38a4.c文件里&#xff0c;定位到Flash_Config {0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFE}; 把最后一個參數0xFFFFFFFE改…

25個讓Java程序員更高效的Eclipse插件

Eclipse提供了一個可擴展插件的開發系統。這就使得Eclipse在運行系統之上可以實現各種功能。這些插件也不同于其他的應用&#xff08;插件的功能是最難用代碼實現的&#xff09;。擁有合適的Eclipse插件是非常重要的&#xff0c;因為它們能讓Java開發者們無縫的開發基于J2EE和服…