javascript
spring 测试 事务_Spring陷阱:事务测试被认为是有害的
spring 測試 事務(wù)
Spring殺手級功能之一是容器內(nèi)集成測試 。 盡管EJB多年來一直缺乏此功能(Java EE 6終于解決了這個問題,但是我尚未進(jìn)行測試),但是Spring從一開始就允許您從Web層開始,通過所有服務(wù)來測試整個堆棧。到數(shù)據(jù)庫的方式。數(shù)據(jù)庫是有問題的部分。 首先,您需要使用內(nèi)存中的獨(dú)立數(shù)據(jù)庫(例如H2)將測試與外部數(shù)據(jù)庫分離。 Spring在很大程度上幫助了這一點(diǎn),尤其是現(xiàn)在有了配置文件和嵌入式數(shù)據(jù)庫支持 。 第二個問題更加微妙。 盡管典型的Spring應(yīng)用程序幾乎是完全無狀態(tài)的(無論好壞),但數(shù)據(jù)庫固有地是有狀態(tài)的。 這使集成測試變得復(fù)雜,因為編寫測試的第一個原則是它們應(yīng)該彼此獨(dú)立且可重復(fù)。 如果一個測試將某些內(nèi)容寫入數(shù)據(jù)庫,則另一個測試可能會失敗; 而且由于數(shù)據(jù)庫更改,相同的測試可能在后續(xù)調(diào)用中失敗。
顯然,Spring還可以通過一個非常巧妙的技巧來解決此問題 :在運(yùn)行每個測試之前,Spring會啟動一個新事務(wù)。 整個測試(包括其設(shè)置和拆除)在同一事務(wù)中運(yùn)行,該事務(wù)在最后回滾。 這意味著測試期間所做的所有更改在數(shù)據(jù)庫中都是可見的,就像它們被持久化一樣。 但是,每次測試后的回滾都會清除所有更改,而下一個測試將在一個全新的數(shù)據(jù)庫上進(jìn)行。 輝煌!
不幸的是,這不是關(guān)于Spring集成測試優(yōu)勢的另一篇文章。 我想我已經(jīng)編寫了成百上千個這樣的測試,而我真的很感謝Spring框架提供的透明支持。 但是我也遇到了這個舒適功能帶來的眾多怪異和不一致之處。 更糟糕的是,通常所謂的事務(wù)測試實際上隱藏了錯誤,使開發(fā)人員確信該軟件可以工作,而部署后卻會失敗! 這是一個不詳盡但令人大開眼界的問題集合:
@Test public void shouldThrowLazyInitializationExceptionWhenFetchingLazyOneToManyRelationship() throws Exception {//givenfinal Book someBook = findAnyExistingBook();//whentry {someBook.reviews().size();fail();} catch(LazyInitializationException e) {//then} }這是Hibernate和spring集成測試中的一個已知問題。 Book是一個數(shù)據(jù)庫實體,與“評論”之間存在一對多的關(guān)系,默認(rèn)情況下是惰性的。 findAnyExistingBook()只是從事務(wù)服務(wù)中讀取測試書。 現(xiàn)在有點(diǎn)理論了:只要將實體綁定到會話(如果使用JPA,則為EntityManager),它就可以延遲并透明地加載關(guān)系。 在我們的情況下,這意味著:只要它在交易范圍之內(nèi)。 實體離開交易的那一刻,它就變得分離。 在此生命周期階段,實體不再連接到session / EntityManager(已經(jīng)提交并關(guān)閉),并且任何獲取懶惰屬性的方法都將引發(fā)可怕的LazyInitializationException。 此行為實際上是在JPA中標(biāo)準(zhǔn)化的(異常類本身除外,它是特定于供應(yīng)商的)。
在本例中,我們正在調(diào)用.reviews()(Scala風(fēng)格的“ getter”,我們也將盡快將測試用例轉(zhuǎn)換為ScalaTest),并期望看到Hibernate異常。 但是,不會引發(fā)異常,并且應(yīng)用程序會繼續(xù)運(yùn)行。 這是因為整個測試都在事務(wù)內(nèi)運(yùn)行,并且Book實體永遠(yuǎn)不會超出事務(wù)范圍。 延遲加載在Spring集成測試中始終有效。
公平地說,我們在現(xiàn)實生活中永遠(yuǎn)不會看到這樣的測試(除非您要進(jìn)行測試以確保給定的集合是惰性的-不太可能)。 在現(xiàn)實生活中,我們正在測試僅在測試中起作用的業(yè)務(wù)邏輯。 但是,在部署之后,我們開始遇到LazyInitializationException。 但是我們測試了! Spring集成測試支持不僅隱藏了該問題 ,而且還鼓勵開發(fā)人員使用OpenSessionInViewFilter或OpenEntityManagerInViewFilter 。 換句話說:我們的測試不僅沒有發(fā)現(xiàn)代碼中的錯誤,而且還大大惡化了我們的整體體系結(jié)構(gòu)和性能。 不是我所期望的。
目前,實現(xiàn)某些端到端功能時,我典型的工作流程是編寫后端測試,實現(xiàn)包括REST API的后端,并在一切運(yùn)行順利時進(jìn)行部署并繼續(xù)使用GUI。 后者是完全使用AJAX / JavaScript編寫的,因此我只需要部署一次并經(jīng)常替換便宜的客戶端文件。 在此階段,我不想回到服務(wù)器來修復(fù)未發(fā)現(xiàn)的錯誤。
抑制LazyInitializationException是Spring集成測試中最著名的問題之一。 但這只是冰山一角。 這是一個更復(fù)雜的示例(它再次使用JPA,但此問題在普通JDBC和任何其他持久性技術(shù)中也很明顯):
@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"); }第一步,我們從數(shù)據(jù)庫中加載一些書并修改作者,然后保存一個實體。 然后,我們通過id在另一個線程中加載相同的實體。 該實體已經(jīng)保存,因此可以確保線程應(yīng)看到更改。 但是,情況并非如此,最后一步中的斷言證明了這一點(diǎn)。 發(fā)生了什么?
我們剛剛在ACID事務(wù)屬性中觀察到“ I”。 在提交事務(wù)之前,測試線程所做的更改對其他線程/連接不可見。 但是我們知道測試事務(wù)已提交! 這個小展示展示了在事務(wù)支持下編寫多線程集成測試有多么困難。 幾周前,當(dāng)我想對啟用JDBCJobStore的 Quartz調(diào)度程序進(jìn)行集成測試時,我學(xué)到了很難的方法。 無論我多么努力,這些工作從未被解雇。 原來,我是在Spring事務(wù)范圍內(nèi)安排在Spring管理的測試中的工作。 由于從未提交過事務(wù),因此外部調(diào)度程序和工作線程無法在數(shù)據(jù)庫中看到新的作業(yè)記錄。 您花了幾個小時調(diào)試此類問題?
在談?wù)撜{(diào)試時,對數(shù)據(jù)庫相關(guān)的測試失敗進(jìn)行故障排除時會彈出相同的問題。 我可以將此簡單的H2 Web控制臺(瀏覽到localhost:8082)bean添加到我的測試配置中:
@Bean(initMethod = "start", destroyMethod = "stop") def h2WebServer() = Server.createWebServer("-webDaemon", "-webAllowOthers")但是在逐步進(jìn)行測試時,我永遠(yuǎn)不會看到測試所做的更改。 我無法手動運(yùn)行查詢以查明為什么返回錯誤結(jié)果。 另外,在進(jìn)行故障排除時,我無法即時修改數(shù)據(jù)以更快地周轉(zhuǎn)。 我的數(shù)據(jù)庫位于另一個維度。
請仔細(xì)閱讀下一個測試,時間不長:
@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"); }我們正在加載一本書并修改作者, 而沒有明確地堅持下去。 然后,我們再次從數(shù)據(jù)庫中加載它,并確保更改未保留。 猜猜是什么,我們已經(jīng)以某種方式更新了對象!
如果您是經(jīng)驗豐富的JPA / Hibernate用戶,那么您將確切地知道如何發(fā)生。 還記得我在上面描述附著/分離實體時的情況嗎? 當(dāng)實體仍附加到基礎(chǔ)EntityManager /會話時,它也具有其他權(quán)力。 JPA提供者有義務(wù)跟蹤對此類實體所做的更改,并在實體分離時將其自動傳播到數(shù)據(jù)庫(所謂的臟檢查)。
這意味著使用JPA實體修改的慣用方式是從數(shù)據(jù)庫中加載對象,使用setter執(zhí)行必要的更改,僅此而已。 當(dāng)實體分離時,JPA將發(fā)現(xiàn)它已被修改并為您發(fā)出UPDATE。 不需要merge()/ update(),可愛的對象抽象。 只要管理實體,此方法就起作用。 對分離的實體所做的更改將被靜默忽略,因為JPA提供程序?qū)Υ祟悓嶓w一無所知。 現(xiàn)在最好的部分–您幾乎不知道您的實體是否已附加,因為事務(wù)管理是透明的并且?guī)缀跏遣豢梢姷摹?這意味著只修改內(nèi)存中的POJO實例,同時仍然認(rèn)為更改是持久的,反之亦然,這太容易了!
我們可以測試嗎? 當(dāng)然,我們只是做了–失敗了。 在我們的測試中,交易涉及整個測試方法,因此每個實體都受到管理。 同樣由于Hibernate L1緩存,即使尚未發(fā)布數(shù)據(jù)庫更新,我們也可以獲取完全相同的圖書實例。 這是事務(wù)測試隱藏問題而不是揭示問題的另一種情況(請參閱LazyInitializationException示例)。 更改將如測試中所預(yù)期的那樣傳播到數(shù)據(jù)庫,但是在部署后會被靜默忽略……
順便說一句,我是否提到,一旦您擺脫了測試用例類的@Transactional批注,到目前為止,所有測試都通過了? 看看,來源永遠(yuǎn)是可用的 。
這是令人興奮的。 我有一個事務(wù)性的deleteAndThrow(book)業(yè)務(wù)方法,該方法刪除給定的書并引發(fā)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> (您是否已經(jīng)注意到Java代碼與用Scala編寫的服務(wù)和實體交互得很好嗎?),而不是null。 如果deleteBook.isEmpty()的結(jié)果為true,則表示未找到結(jié)果。 因此,似乎我們的代碼是正確的:實體已刪除,并且引發(fā)了異常。 是的,您是正確的,它在再次部署后會靜默失敗! 這次Hibernate L1緩存知道該特定的book實例已刪除,因此即使在刷新更改到數(shù)據(jù)庫之前它也返回null。 但是,從服務(wù)拋出的OppsException會回滾事務(wù),并丟棄DELETE! 但是測試通過了,只是因為Spring管理著這個微小的額外事務(wù),并且斷言發(fā)生在該事務(wù)內(nèi)。 毫秒后,事務(wù)回滾,恢復(fù)已刪除的實體。
顯然,解決方案是為OppsException添加noRollbackFor屬性(這是我在放棄事務(wù)性測試以支持其他解決方案后在我的代碼中發(fā)現(xiàn)的實際錯誤,目前尚待解釋)。 但這不是重點(diǎn)。 關(guān)鍵是– 您真的可以負(fù)擔(dān)起編寫和維護(hù)正在生成假陽性的測試,說服您的應(yīng)用程序正常運(yùn)行的能力,而事實并非如此?
哦,我是否提到過跨語言測試實際上在這里和那里泄漏,并且不會阻止您修改測試數(shù)據(jù)庫? 第二次測試失敗,您知道為什么嗎?
@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(); }線程再次陷入困境。 當(dāng)您嘗試在顯然已提交的后臺線程中進(jìn)行外部事務(wù)處理后進(jìn)行清理時,它會變得更加有趣。 自然的地方是在@After方法中刪除創(chuàng)建的Review。 但是@After是在同一測試事務(wù)中執(zhí)行的,因此清理將…回滾。
當(dāng)然,我并不是在抱怨我最喜歡的應(yīng)用程序堆棧弱點(diǎn)。 我在這里提供解決方案和提示。 我們的目標(biāo)是完全擺脫事務(wù)測試,僅依賴于應(yīng)用程序事務(wù)。 這將有助于我們避免上述所有問題。 顯然,我們不能放棄測試的獨(dú)立性和可重復(fù)性功能。 每個測試必須在同一數(shù)據(jù)庫上工作才能可靠。 首先,我們將把JUnit測試轉(zhuǎn)換為ScalaTest。 為了獲得Spring依賴注入支持,我們需要這個微小的特征:
trait SpringRule extends Suite with BeforeAndAfterAll { this: AbstractSuite =>override protected def beforeAll() {new TestContextManager(this.getClass).prepareTestInstance(this)super.beforeAll();}}現(xiàn)在是時候揭示我的想法了(如果您不耐煩,請在此處查看完整的源代碼 )。 它遠(yuǎn)非獨(dú)創(chuàng)性或獨(dú)特性,但我認(rèn)為它值得關(guān)注。 無需在一個巨大的事務(wù)中運(yùn)行所有內(nèi)容并將其回滾,只需讓經(jīng)過測試的代碼在需要和配置的任何地方,任何時間啟動和提交事務(wù)即可。 這意味著數(shù)據(jù)實際上已寫入數(shù)據(jù)庫,并且持久性的工作原理與部署后完全相同。 漁獲物在哪里? 每次測試后,我們都必須以某種方式清理混亂……
事實證明它并不那么復(fù)雜。 只需轉(zhuǎn)儲干凈的數(shù)據(jù)庫,然后在每次測試后將其導(dǎo)入! 轉(zhuǎn)儲包含在部署和應(yīng)用程序啟動之后,第一次測試運(yùn)行之前立即存在的所有表,約束和記錄。 就像備份并從中還原一樣! 看看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 SpringRuleSQL轉(zhuǎn)儲(請參閱H2 SCRIPT命令)執(zhí)行一次并導(dǎo)出到臨時文件。 然后在每次測試后執(zhí)行SQL腳本文件。 信不信由你,就是這樣! 我們的測試不再是事務(wù)性的(因此,所有Hibernate和多線程的極端情況都已被發(fā)現(xiàn)和測試),而我們并沒有犧牲事務(wù)性測試設(shè)置的簡便性(不需要額外的清理)。 我最終還可以在調(diào)試時查看數(shù)據(jù)庫內(nèi)容! 這是進(jìn)行中的先前測試之一:
@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)} }請記住,這不是庫/實用程序,而是一個想法。 對于您的項目,您可能會選擇略有不同的方法和工具,但是總體思路仍然適用:讓您的代碼在與部署后完全相同的環(huán)境中運(yùn)行,然后從備份中清除混亂。 您可以使用JUnit, HSQLDB或任何您喜歡的東西獲得完全相同的結(jié)果。 當(dāng)然,您也可以添加一些巧妙的優(yōu)化方法-標(biāo)記或發(fā)現(xiàn)未修改數(shù)據(jù)庫的測試,選擇更快的轉(zhuǎn)儲,導(dǎo)入方法等。
老實說,還有一些弊端,以下是我腦海中的一些缺點(diǎn):
- 性能 :盡管這種方法并不總是比回滾事務(wù)慢得多(某些數(shù)據(jù)庫回滾的速度特別慢),但并不明顯,但可以肯定地說。 當(dāng)然,內(nèi)存數(shù)據(jù)庫可能具有一些意外的性能特征,但要為速度變慢做好準(zhǔn)備。 但是,我沒有在一個小項目中觀察到每100個測試有巨大的差異(大約10%)。
- 并發(fā) :您不再可以同時運(yùn)行測試。 一個線程(測試)所做的更改對其他線程可見,從而使測試執(zhí)行無法預(yù)測。 對于上述性能問題,這甚至變得更加痛苦。
就是這樣。 如果您有興趣,請給這種方法一個機(jī)會。 采用您現(xiàn)有的測試基礎(chǔ)可能需要一些時間,但是即使發(fā)現(xiàn)一個隱藏的錯誤也值得,您是不是認(rèn)為呢? 并且還要注意其他Spring陷阱 。
參考: 春天的陷阱: NoBlogDefFound Blog上的 JCG合作伙伴 Tomasz Nurkiewicz 認(rèn)為有害的事務(wù)測試 。
相關(guān)文章 :- Spring陷阱:代理
- Spring聲明式事務(wù)示例
- Spring依賴注入技術(shù)的發(fā)展
- Spring和AspectJ的領(lǐng)域驅(qū)動設(shè)計
- Spring 3使用JUnit 4進(jìn)行測試– ContextConfiguration和AbstractTransactionalJUnit4SpringContextTests
- 使用Spring AOP進(jìn)行面向方面的編程
- Java教程和Android教程列表
翻譯自: https://www.javacodegeeks.com/2011/12/spring-pitfalls-transactional-tests.html
spring 測試 事務(wù)
總結(jié)
以上是生活随笔為你收集整理的spring 测试 事务_Spring陷阱:事务测试被认为是有害的的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: (linux的usb驱动)
- 下一篇: 比较中的Commons VFS,SSHJ