重新同步多线程集成测试
我最近在Captain Debug的Blog上偶然發現了一篇文章“ 同步多線程集成測試 ”。 該文章強調了設計涉及被測類以異步方式運行業務邏輯的集成測試的問題。 給出了這個人為的示例(我刪除了一些評論):
這只是常見模式的一個示例,在該模式中,業務邏輯被委派給我們無法控制的某些異步作業池。 Roger Hughes (作者)列舉了測試這種代碼的幾種技巧,包括:
- 測試方法中的任意(“足夠長”) sleep()以確保后臺邏輯完成
- 重構doWork() ,使其接受CountDownLatch并同意在作業完成時通知它
- 將上述方法@VisibleForTesting包私有且僅@VisibleForTesting
- “該”解決方案–重構doWork()使其可以接受任意Runnable 。 在測試中,我們可以包裝此Runnable (裝飾器模式)并等待內部Runnable完成
最后一個解決方案不錯,但是它極大地改變了ThreadWrapper的職責。 現在,由調用者決定在先前ThreadWrapper完全封裝業務邏輯時應異步執行哪種作業。 我并不是說這是一個糟糕的設計,但是它與原始方法有很大的不同。
待命性
如果不進行如此大規模的重構,我們可以編寫測試嗎? 第一個解決方案涉及稱為Awaitility的便捷庫。 該庫不是靈丹妙藥,它只是定期評估給定條件并確保它在給定時間內得到滿足。 您可能會編寫一兩次這樣的代碼,然后將它們包裝在具有精心設計的API的庫中。 因此,這是我們的初始方法:
import static com.jayway.awaitility.Awaitility.await; import static java.util.concurrent.TimeUnit.SECONDS;//...await().atMost(10, SECONDS).until(recordInserted());//...private Callable<Boolean> recordInserted() {return new Callable<Boolean>() {@Overridepublic Boolean call() throws Exception {return dataExists();}}; }我認為這里沒有什么可解釋的。 dataExists()只是一個boolean方法,最初返回false但一旦后臺任務( addDataToDB() )完成,最終將返回true 。 換句話說,我們假設后臺任務引入了一些副作用,而dataExists()可以檢測到該副作用。 順便說一句,我碰巧安裝了具有Lambda支持的JDK 8,而IntelliJ IDEA給了我這個很好的工具提示:
突然,我得到了建議的與Java 8兼容的替代方案:
但是還有更多:
將我的代碼轉換為:
private Callable<Boolean> recordInserted() {return this::dataExists; }this::前綴表示recordInsterted是當前對象的方法。 我們也可以說someDao::dataExists 。 簡單地將此語法turns方法放入我們可以傳遞的函數對象中(此過程在Scala中稱為eta擴展 )。 到目前為止,不再需要recordInsterted()方法,因此我可以內聯它并將其完全刪除:
await().atMost(10, SECONDS).until(this::dataExists);我不確定我更喜歡什么-新的lambda語法或IntelliJ IDEA如何采用Java 8之前的代碼并自動為我進行改版 (嗯,這仍然有點試驗性,剛剛報道了IDEA-106670 )。 我可以在IntelliJ項目級的Lambda中運行此意圖,從而在幾秒鐘內啟用整個代碼庫。 甜!
但是回到原來的問題。 通過提供體面的API和一些方便的功能,Awaitility很有幫助。 我將它與FluentLenium廣泛結合使用。 但是定期輪詢狀態變化感覺有點像變通辦法,并且仍然引入了最小的延遲。 但是請注意,在異步任務上運行和同步非常普遍,并且JDK已經提供了必要的功能: Future抽象 !
java.util.concurrent.Future
為了限制重構的范圍,我將暫時保留原始的new Thread()方法,并使用Guava中的SettableFuture<V> 。 它是Future<V>實現,允許在任何時間從任何線程觸發完成或失敗(請參閱DeferredResult – Spring MVC中的異步處理以獲取更多高級用法)。 如您所見,更改非常小:
public class ThreadWrapper {public ListenableFuture<Void> doWork() {final SettableFuture<Void> future = SettableFuture.<Void>create();Thread thread = new Thread() {@Overridepublic void run() {addDataToDB()//...//last instructionfuture.set(null);}private void addDataToDB() {// Dummy Code...// ...}};thread.start();return future;}}doWork()現在返回在異步任務內部控制生命周期的ListenableFuture<Void> 。 我們使用Void但實際上您可能想返回一些異步結果。 future.set(null)調用至關重要。 它表示將來已實現,并且將通知所有等待該將來的線程。 再一次,在實踐中,您將使用例如Future<Integer> ,然后我們將說future.set(someInteger)而不是null 。 這里null只是Void類型的占位符。 這對我們有什么幫助? 測試代碼現在可以依賴將來的完成:
final ListenableFuture<Void> future = wrapper.doWork(); future.get(10, SECONDS);future.get()阻塞,直到將來完成(超時),即直到我們調用future.set(...)為止。 順便說一句,我使用的是Guava的ListenableFuture ,但是Java 8引入了等效和標準的CompletableFuture –我將很快寫它。
所以,我們到了某個地方。 Future<T>是用于等待和發信號通知后臺作業完成的有用抽象。 但也有一個巨大的優勢, Future未服用,ekhm,從優勢-異常處理和傳播。 Future.get()將阻塞,直到Future.get()完成,并返回異步結果或引發最初從我們的工作中引發的異常。 這對于異步測試非常有用。 當前,如果Thread.run()引發異常,則它可能會記錄也可能不會記錄或對我們可見,并且將來將永遠無法完成。 使用Awaitility會稍微好一些-它會超時而沒有任何有意義的原因,必須在控制臺/日志中手動進行跟蹤。 但是,只需稍作修改,我們的測試就會更加冗長:
public void run() {try {addDataToDB()//...future.set(null);} catch (Exception e) {future.setException(e);} }如果異步作業中發生某些異常,它將彈出并顯示為JUnit / TestNG失敗原因。
(聽)ExecutorService
而已。 如果addDataToDB()引發異常,它將不會丟失。 相反,測試中的future.get()將為我們重新拋出該異常。 我們的測試不會只是超時,也不會讓我們知道發生了什么問題。 太好了,但是我們真的必須創建這個特殊的SettableFuture<T>實例,難道我們不能僅使用已經為我們提供Future<T>并具有正確基礎實現的現有庫嗎? 當然! 這樣就需要進一步重構:
import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors;import java.util.concurrent.Executors; import java.util.concurrent.Future;public class ThreadWrapper {private final ListeningExecutorService executorService =MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor());public ListenableFuture<?> doWork() {Runnable job = new Runnable() {@Overridepublic void run() {//...}};return executorService.submit(job);}}這就是你們一直在等待的東西。 不要一直啟動新Thread ,請使用線程池! 實際上,我通過使用ListeningExecutorService (對ExecutorService的擴展)返回了ListenableFuture實例( 請參閱為什么 ListenableFuture ,進一步走了一步。 但是解決方案不需要這樣做,我只是傳播了良好的做法。 如您所見,現在已經為我們創建并管理了Future實例。 測試完全相同,但是生產代碼更簡潔,更可靠。
MoreExecutors.sameThreadExecutor()
我想向您展示的最后一個技巧是依賴注入。 首先,讓我們從ThreadWrapper類外部化線程池的創建:
private final ListeningExecutorService executorService;public ThreadWrapper() {this(Executors.newSingleThreadExecutor()); }public ThreadWrapper(ExecutorService executorService) {this.executorService =MoreExecutors.listeningDecorator(executorService); }現在,我們可以選擇提供自定義ExecutorService 。 出于其他各種原因,這是件好事,但是對我們來說,這提供了全新的測試機會: MoreExecutors.sameThreadExecutor() 。 這次我們稍微修改一下測試:
final ThreadWrapper wrapper = new ThreadWrapper(MoreExecutors.sameThreadExecutor()); wrapper.doWork().get();看看我們如何通過自定義ExecutorService ? 這是一個非常特殊的實現,它實際上并不維護任何類型的線程池。 每次您向該“池” submit()某個任務時,它將以阻塞方式在同一線程中執行。 這意味著即使生產代碼沒有太大變化,我們也不再需要異步測試! wrapper.doWork()將阻塞,直到“后臺”作業完成。 仍然需要對get()額外的調用,以確保傳播了異常,但是保證永遠不會阻塞(因為作業已經完成)。
如果您某種程度上依賴于基于線程的屬性(例如事務,安全性, ThreadLocal ,則使用同一線程而不是線程池來執行異步任務可能會產生意外結果。 但是,如果您將標準ThreadPoolExecutor與CallerRunsPolicy一起CallerRunsPolicy ,則在線程池溢出的情況下,JDK會以這種方式運行。 因此,這并不罕見。
摘要
測試異步代碼很困難,但是您可以選擇。 幾種選擇。 但是令我印象深刻的一個結論是我們努力的副作用。 我們重構了原始代碼以使其可測試。 但是最終的生產代碼不僅可以測試,而且結構和健壯性也更好。 令人驚訝的是,它甚至與以前的版本兼容,因為我們幾乎沒有將返回類型從void更改為Future<Void> 。
這似乎是一條規則-可測試的代碼通常可以更好地設計和實現。 單元測試是使用我們庫的第一個客戶端代碼。 這自然迫使我們要更多地考慮消費者,而不是實現。
翻譯自: https://www.javacodegeeks.com/2013/05/synchronising-multithreaded-integration-tests-revisited.html
總結
以上是生活随笔為你收集整理的重新同步多线程集成测试的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 安卓 无线显示器(安卓 无线显示)
- 下一篇: (linux $date)