偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

關(guān)于寫異步代碼測試用例的一些思考

開發(fā) 前端
如果說異步代碼不好寫是共識的話,那么寫異步代碼測試用例就更難了。最近我剛剛完成了一個 Flaky 測試,所以想和大家分享一些關(guān)于寫異步測試用例的想法。

如果說異步代碼不好寫是共識的話,那么寫異步代碼測試用例就更難了。最近我剛剛完成了一個 Flaky 測試,所以想和大家分享一些關(guān)于寫異步測試用例的想法。

這篇文章里,我們會探索一個關(guān)于異步測試用例的常見問題 —— 如何強制規(guī)定某些線程的順序,如何強制某一個線程操作早于另一些執(zhí)行。通常我們并不想強行規(guī)定線程之間的順序,因為這違背了多線程的原則,所謂多線程就是 為了做到并發(fā),從而使得 CPU 可以根據(jù)當前資源及應(yīng)用狀態(tài)選擇最佳的執(zhí)行順序。但是在測試中,為了確保測試結(jié)果的穩(wěn)定性,又必須明確線程順序。

[[151574]]

測試節(jié)流閥(Throttler)

在軟件業(yè)里節(jié)流閥指的是用于限制并發(fā)操作個數(shù),預(yù)留資源的模式,好比連接池,網(wǎng)絡(luò)緩存,或者 CPU 密集型操作。和其他同步工具不同的是,節(jié)流閥的角色是啟動“快速失敗”機制,即促使超額請求立即失敗,而不是等待。“快速失敗”機制之所以重要,是因為切 換操作,等待操作會消耗資源 —— 端口,線程,內(nèi)存等。

以下就是一個節(jié)流閥的簡單實現(xiàn)(基本上是信號量的包裝,實際應(yīng)用中應(yīng)該是等待,重試等等)

 

  1. class ThrottledException extends RuntimeException("Throttled!"
  2. class Throttler(count: Int) { 
  3.   private val semaphore = new Semaphore(count) 
  4.   def apply(f: => Unit): Unit = { 
  5.     if (!semaphore.tryAcquire()) throw new ThrottledException 
  6.     try { 
  7.       f 
  8.     } finally { 
  9.       semaphore.release() 
  10.     } 
  11.   } 

現(xiàn)在我們開始基本的單元測試:測試單線程的節(jié)流閥(我們使用測試框架 specs2)。本例里,我們會驗證順序調(diào)用是否會超過節(jié)流閥的最大限制(maxCount變量如下所示)。注意,這里我們用的是單線程,所以我們并不驗證節(jié)流閥的“快速失敗”功能,這里的節(jié)流閥都處于不飽和狀態(tài)。事實上,我們只會測試節(jié)流閥在不飽和狀態(tài)下不會終止操作。

 

  1. class ThrottlerTest extends Specification { 
  2.   "Throttler" should { 
  3.     "execute sequential" in new ctx { 
  4.       var invocationCount = 0 
  5.       for (i <- 0 to maxCount) { 
  6.         throttler { 
  7.           invocationCount += 1 
  8.         } 
  9.       } 
  10.       invocationCount must be_==(maxCount + 1
  11.     } 
  12.   } 
  13.   trait ctx { 
  14.     val maxCount = 3 
  15.     val throttler = new Throttler(maxCount) 
  16.   } 

測試并發(fā)節(jié)流閥

前一個例子里,節(jié)流閥處于不飽和狀態(tài),因為單線程里節(jié)流閥一般都不會飽和。下面我們來測試一下多線程環(huán)境下節(jié)流閥是否還能工作良好。

設(shè)置如下:

 

  1. val e = Executors.newCachedThreadPool() 
  2. implicit val ec: ExecutionContext=ExecutionContext.fromExecutor(e) 
  3. private val waitForeverLatch = new CountDownLatch(1
  4.  
  5. override def after: Any = { 
  6.   waitForeverLatch.countDown() 
  7.   e.shutdownNow() 
  8.  
  9. def waitForever(): Unit = try { 
  10.   waitForeverLatch.await() 
  11. catch { 
  12.   case _: InterruptedException => 
  13.   case ex: Throwable => throw ex 

 

ExecutionContext 用來構(gòu)建 Future,waitForever 方法用來持有線程,直到測試結(jié)束前的鎖釋放。接下來的函數(shù)里,我們會關(guān)閉一個執(zhí)行服務(wù)。

以下就是一個測試節(jié)流器多線程行為的例子:

 

  1. "throw exception once reached the limit [naive,flaky]" in new ctx { 
  2.   for (i <- 1 to maxCount) { 
  3.     Future { 
  4.       throttler(waitForever()) 
  5.     } 
  6.   } 
  7.   throttler {} must throwA[ThrottledException] 

我們創(chuàng)建了 maxCount 個線程(調(diào)用 Future{})來調(diào)用 waitForever 函數(shù),該函數(shù)會一直直到道測試結(jié)束。然后我們繞開節(jié)流閥執(zhí)行另一個操作 —— maxCount + 1。預(yù)期的行為是,此時應(yīng)該拋出 ThrottledException 例外。但是,也許預(yù)期的例外并不發(fā)生,因為接力器的最后的一個調(diào)用可能會比 future 里的先執(zhí)行(future 里會拋出例外,但是這不是預(yù)期結(jié)果)。

上面這個測試的問題是,在像期望中那樣節(jié)流閥拋出異常然后導致節(jié)流閥被違反之前,我們無法確定所有的線程都已經(jīng)開始并且在 waitForever 函數(shù)中被阻塞。為了修復(fù)這個問題,我們需要一些方法去等待所有 future 開始。這有一個我們大多數(shù)都很熟悉的一種方法:只要增加一個 sleep 函數(shù)等待一些合適的時間。

 

  1. "throw exception once reached the limit [naive, bad]" in new ctx { 
  2.   for (i <- 1 to maxCount) { 
  3.     Future { 
  4.       throttler(waitForever()) 
  5.     } 
  6.   } 
  7.   Thread.sleep(1000
  8.   throttler {} must throwA[ThrottledException] 

好了,現(xiàn)在這個測試幾乎都能通過了,但是這個方法還是錯的因為下面這兩個原因:

測試持續(xù)的時間至少會和我們設(shè)置好的”合適的時間”差不多久。

在非常罕見的情況下,比如機器處于高負載的時候,這個合適的時間不一定足夠。

如果你仍然感到疑惑,可以搜索一下 Google 更多的原因。

一個更好的方式是將我們的線程(future)的開始和我們期望的東西同步起來。我們來使用 java.util.concurrent 里面的 CountDownLatch 類:

 

  1. "throw exception once reached the limit [working]" in new ctx { 
  2.   val barrier = new CountDownLatch(maxCount) 
  3.  
  4.   for (i <- 1 to maxCount) { 
  5.     Future { 
  6.       throttler { 
  7.         barrier.countDown() 
  8.         waitForever() 
  9.       } 
  10.     } 
  11.   } 
  12.  
  13.   barrier.await(5, TimeUnit.SECONDS) must beTrue 
  14.  
  15.   throttler {} must throwA[ThrottledException] 

 

我們使用 CountDownLatch 處理障礙同步。 這個等待的方法會阻塞主線程直到鎖存計數(shù)變?yōu)?0。隨著其它線程的運行(我們把這些其它線程表示為 future),每一個 future 都會調(diào)用 countDown 方法使鎖存計數(shù)減 1。一但計數(shù)變?yōu)?0,所有的 future 就都已經(jīng)運行到 waitForever 方法中了。

通過那一點,我們可以確保 throttler 是飽和的,內(nèi)部有最大數(shù)量(maxCount)的線程。另一個線程試圖進入 throttler 將導致異常。我們有一個確定的方式建立我們的測試,測試會有一個主線程進入 throttler。主線程可以恢復(fù)到這個點(門閂計數(shù)為 0 并等 CountDownLatch 釋放等待線程)。

如果一些意想不到的事情發(fā)生,我們使用超時略高保障避免無限阻塞發(fā)生。如果這樣的事情發(fā)生,我們的測試就失敗了。這個超時不會影響到測試時間,除非發(fā)生意外情況,否則,我們都不應(yīng)該等待。

結(jié)論

測試異步程序時,通常需要在具體的測試用例中指定多個線程之間的執(zhí)行順序。不使用任何同步策略的測試是不可靠的,測試結(jié)果有時成功有時失敗。使用 Thread.sleep 降低了測試出錯的概率,但沒有完全解決這個問題。

在大多數(shù)情況下,當需要在測試中保證多個線程的執(zhí)行順序時,可以使用 CountDownLatch 代替 Thead.sleep。使用 CountDownlatch 的好處是通過它可以指定釋放(保持)線程的時機,有兩個優(yōu)點:確保按順序執(zhí)行使測試結(jié)果更可靠;加快了測試程序的執(zhí)行速度。即使對于普通的 waiting 操作,比如 waitForever 函數(shù),盡管也可以使用 Thread.sleep(Long.MAX_VALUE) 這樣的函數(shù)實現(xiàn),但為了保證程序的健壯性最好不要這樣做。

完整的代碼可以在 GitHub 中找到。

責任編輯:王雪燕 來源: oschina
相關(guān)推薦

2012-12-19 09:36:49

測試自動化測試

2021-06-15 07:10:14

JavaScript異步編程

2021-03-04 15:43:29

前端測試工具開發(fā)

2021-06-10 10:02:19

優(yōu)化緩存性能

2017-12-21 07:54:07

2020-08-20 10:16:56

Golang錯誤處理數(shù)據(jù)

2024-12-27 10:51:53

2020-02-03 16:03:36

疫情思考

2009-06-25 09:50:32

JSF

2011-05-31 17:50:07

白盒測試

2018-06-29 14:51:41

Java健壯性實踐

2021-08-08 10:44:33

安卓系統(tǒng)開發(fā)者手機廠商

2021-06-10 20:17:04

云網(wǎng)融合超融合

2011-11-30 15:57:18

2011-01-19 10:50:31

軟件設(shè)計師

2011-07-13 09:13:56

Android設(shè)計

2011-06-14 14:04:11

測試用例

2011-08-01 10:37:29

軟件項目管理

2018-06-14 09:35:35

點贊
收藏

51CTO技術(shù)棧公眾號