同步多线程集成测试
測試線程非常困難,這使得為要測試的多線程系統編寫良好的集成測試非常困難。 這是因為在JUnit中,測試代碼,被測對象和任何線程之間沒有內置的同步。 這意味著,當您必須為創建并運行線程的方法編寫測試時,通常會出現問題。 該領域中最常見的場景之一是調用被測方法,該方法在返回之前啟動新線程的運行。 在將來某個時刻完成線程的工作時,您需要斷言一切都很好。 這種情況的示例可能包括異步地從套接字讀取數據或對數據庫執行一系列漫長而復雜的操作。
例如,下面的ThreadWrapper類包含一個公共方法: doWork() 。 調用doWork()會使情況doWork()并且在將來某個時候,由JVM決定,線程會運行,將數據添加到數據庫中。
此代碼的直接測試是調用doWork()方法,然后在數據庫中檢查結果。 問題在于,由于使用了線程,被測對象,測試對象與線程之間沒有協調。 編寫此類測試時,實現某種協調的一種常見方法是在被測方法的調用與檢查數據庫中的結果之間放置某種延遲,如下所示:
public class ThreadWrapperTest {@Testpublic void testDoWork() throws InterruptedException {ThreadWrapper instance = new ThreadWrapper();instance.doWork();Thread.sleep(10000);boolean result = getResultFromDatabase();assertTrue(result);}/*** Dummy database method - just return true*/private boolean getResultFromDatabase() {return true;} }在上面的代碼中,兩個方法調用之間有一個簡單的Thread.sleep(10000) 。 這種技術的優點是簡單易行。 但是它也非常危險。 這是因為它在測試和工作線程之間引入了競爭條件,因為JVM無法保證線程何時運行。 通常,它只能在開發人員的計算機上工作,而在構建計算機上始終失敗。 即使可以在構建機器上運行,它也會從表面上延長測試時間; 請記住,快速構建很重要。 正確實現此操作的唯一肯定方法是同步兩個不同的線程,而執行此操作的一種技術是將一個簡單的CountDownLatch注入被測實例。 在下面的示例中,我修改了ThreadWrapper類的doWork()方法,將CountDownLatch添加為參數。
public class ThreadWrapper {/*** Start the thread running so that it does some work.*/public void doWork(final CountDownLatch latch) {Thread thread = new Thread() {/*** Run method adding data to a fictitious database*/@Overridepublic void run() {System.out.println("Start of the thread");addDataToDB();System.out.println("End of the thread method");countDown();}private void addDataToDB() {try {Thread.sleep(4000);} catch (InterruptedException e) {e.printStackTrace();}}private void countDown() {if (isNotNull(latch)) {latch.countDown();}}private boolean isNotNull(Object obj) {return latch != null;}};thread.start();System.out.println("Off and running...");} }Javadoc API將倒數鎖存器描述為:同步輔助,它允許一個或多個線程等待,直到在其他線程中執行的一組操作完成為止。 CountDownLatch用給定的計數初始化。 由于countCount()方法的調用,直到當前計數達到零為止,await方法將阻塞,此后所有釋放的線程將被釋放,并且所有隨后的await調用將立即返回。 這是一種一次性現象,無法重置計數。 如果需要用于重置計數的版本,請考慮使用CyclicBarrier。
CountDownLatch是一種多功能的同步工具,可以用于多種目的。 以1計數初始化的CountDownLatch用作簡單的開/關閂鎖或門:所有調用await的線程在門處等待,直到被調用countDown()的線程打開為止。 初始化為N的CountDownLatch可以用于使一個線程等待,直到N個線程完成某項操作或某項操作已完成N次。 CountDownLatch的一個有用屬性是,它不需要調用countDown的線程在繼續進行操作之前就不必等待計數達到零,它只是防止任何線程經過等待狀態,直到所有線程都可以通過。
這里的想法是,直到工作線程的run()方法調用latch.countdown() ,測試代碼才會檢查數據庫的結果。 這是因為測試代碼線程正在阻塞對latch.await()的調用。 閂鎖latch.countdown()減少閂鎖的計數,并且一旦它為零,阻塞調用閂鎖latch.await()將返回并且測試代碼將繼續執行,這是安全的, latch.await()是應知道數據庫中應有任何結果。 然后,測試可以檢索這些結果并做出有效的斷言。 顯然,上面的代碼只是偽造數據庫連接和操作。 問題是您可能不想或不需要將CountDownLatch直接插入代碼中。 畢竟它沒有在生產中使用,看起來也不是特別干凈或優雅。 解決此問題的一種快速方法是簡單地使doWork(CountDownLatch latch)方法包私有,并通過公共doWork()方法公開它。
public class ThreadWrapper {/*** Start the thread running so that it does some work.*/public void doWork() {doWork(null);}@VisibleForTestingvoid doWork(final CountDownLatch latch) {Thread thread = new Thread() {/*** Run method adding data to a fictitious database*/@Overridepublic void run() {System.out.println("Start of the thread");addDataToDB();System.out.println("End of the thread method");countDown();}private void addDataToDB() {try {Thread.sleep(4000);} catch (InterruptedException e) {e.printStackTrace();}}private void countDown() {if (isNotNull(latch)) {latch.countDown();}}private boolean isNotNull(Object obj) {return latch != null;}};thread.start();System.out.println("Off and running...");} }上面的代碼使用Google的Guava @VisibleForTesting批注來告訴我們,出于測試目的,已經稍微放松了doWork(CountDownLatch latch)方法的可見性。
現在,我意識到,將一個方法調用包私有化以用于測試目的是非常有爭議的; 有些人討厭這個主意,而另一些人則無所不在。 我可以寫一個關于這個主題的整個博客(可能一天),但是對我來說,在別無選擇的情況下(例如,當您為遺留代碼編寫特性測試時),應謹慎使用。 如果可能,應避免使用它,但決不能排除。 畢竟,經過測試的代碼比未經測試的代碼更好。
考慮到這一點, ThreadWrapper的下一個迭代將設計出標記為@VisibleForTesting的方法,以及將CountDownLatch注入生產代碼的需求。 這里的想法是使用策略模式并將Runnable實現與Thread分開。 因此,我們有一個非常簡單的ThreadWrapper
public class ThreadWrapper {/*** Start the thread running so that it does some work.*/public void doWork(Runnable job) {Thread thread = new Thread(job);thread.start();System.out.println("Off and running...");} }和一個單獨的工作:
public class DatabaseJob implements Runnable {/*** Run method adding data to a fictitious database*/@Overridepublic void run() {System.out.println("Start of the thread");addDataToDB();System.out.println("End of the thread method");}private void addDataToDB() {try {Thread.sleep(4000);} catch (InterruptedException e) {e.printStackTrace();}} }您會注意到DatabaseJob類不使用CountDownLatch 。 如何同步? 答案就在下面的測試代碼中……
public class ThreadWrapperTest {@Testpublic void testDoWork() throws InterruptedException {ThreadWrapper instance = new ThreadWrapper();CountDownLatch latch = new CountDownLatch(1);DatabaseJobTester tester = new DatabaseJobTester(latch);instance.doWork(tester);latch.await();boolean result = getResultFromDatabase();assertTrue(result);}/*** Dummy database method - just return true*/private boolean getResultFromDatabase() {return true;}private class DatabaseJobTester extends DatabaseJob {private final CountDownLatch latch;public DatabaseJobTester(CountDownLatch latch) {super();this.latch = latch;}@Overridepublic void run() {super.run();latch.countDown();}} }上面的測試代碼包含一個內部類DatabaseJobTester ,該類擴展了DatabaseJob 。 在此類中,在通過調用super.run()更新了虛假數據庫之后,將run()方法重寫為包括對latch.countDown()的調用。 之所以doWork(Runnable job) ,是因為測試將DatabaseJobTester實例傳遞給doWork(Runnable job)方法,并添加了所需的線程測試功能。 我曾在我的一篇有關測試技術的博客中提到過將被測對象分類的想法,這是一種非常強大的技術。
因此,得出以下結論:
- 測試線程很難。
- 測試匿名內部類幾乎是不可能的。
- 使用Thead.sleep(...)是一個冒險的想法,應避免使用。
- 您可以使用策略模式來重構這些問題。
- 編程是做出正確決策的藝術
…放松測試方法的可視性可能不是一個好主意,但稍后會更多……
上面的代碼可在unit-testing-threads項目下的隊長調試存儲庫(git://github.com/roghughe/captaindebug.git)中的Github上找到。
參考: Captain Debug的Blog博客中的JCG合作伙伴 Roger Hughes的同步多線程集成測試 。
翻譯自: https://www.javacodegeeks.com/2013/02/synchronising-multithreaded-integration-tests.html
總結
- 上一篇: 膨胀的JavaBeans –不要在您的A
- 下一篇: 汽车之家亮相2023服贸会 探讨新能源汽