今天組里的小伙伴問了我一個問題:“我這里有一個底層驅動的接口,我想在測試它的時候加上超時限制,時間一過就fail掉它,執行后面的測試用例。怎么辦到呢?”。我問:“它自己沒有超時響應的機制么? 超時拋exception或者返回錯誤提示什么的?”,小伙伴回答是“好像沒有。” 我接着問: “這個接口是做什么的,是核心交易么?” “算是吧,調用還挺頻繁的。”小伙伴回答。“那這個接口決不能讓它通過測試啊!”我大聲回答,旁邊n人側目。“好吧。那我如何實現超時fail呢?” 小伙伴繼續問。。。“呃。。。讓我慢慢道來。”
超時處理其實是編程過程中經常要面對的問題。在我們調用某個函數的時候,調用方把控制權交給了被調用方,但被調用方很多時候是不可控的,如果被調用方長時間不給調用方返回結果,調用方就要想別的辦法,不然就hang死在那里了。這就是超時處理最初始的需求,它的本質是要求把同步調用變成異步調用。把同步變異步其實是個較大的話題,不同的高級語言,框架,甚至操作系統都進行了各式各樣的封裝,提供了各式各樣的接口,十分精彩,但這並不是今天的重點,因此不會展開說了。下面舉幾個例子來看看“timeout”是怎么實現的。先拿JAVA語言來說吧:
首先要說明的是,在單線程下,不借助一些特殊工具,“超時處理”是很難實現的,請見下面的代碼:
RemoteServer itest = new RemoteServer() String result = itest.callRemote() //callRemote()是一個遠端接口
如果callRemote()方法永遠不給返回值,那程序就一直停留 result = itest.callRemote() 這一行不往下走了。
如何實現下面語法中想要的結果呢,如果callRemote()永遠不會拋出TimeoutException的話?
try{
RemoteServer itest = new RemoteServer()
result = itest.callRemote()
}catch(TimeoutException e)
{ e.printStackTrace() }
多線程?這是個好主意!你最初的想法可能是:讓一個子線程在調用前開始計時,如果超時了通知主線程。如果是我,我會一般想到兩種通知的方式:一種是拋出異常,讓主線程捕獲,另一種是Listener的方式實現callback。
如果你使用第一種拋異常的方式,見如下代碼:
public class TimeCount implements Runnable { private long timeOut; private long beginTime; public TimeCount(long timeOut,long beginTime){ this.timeOut = timeOut; this.beginTime = beginTime; } public void run() throws TimeOutException{ while(true) { if((System.currentTimeMillis()-beginTime)>timeOut){ throw new TimeOutException("Timeout!"); } } } }
恭喜你,Java不允許run() 方法向上拋出異常。就算你@override 它也不行。這也是JDK早期的線程模型一個重要槽點。
OK,那我只能使用listener的方式了。
Private XXListener lstr; public void run() throws TimeOutException{ while(true) { if((System.currentTimeMillis()-beginTime)>timeOut){ lstr.notify("Time out!"); } } }
但是這樣如果沒有現成的Listener,你就要去實現它,還是很復雜的(有興趣可以看看這篇文章),同時,對被測類產生了一定入侵。可見,上面兩種方法都不是什么好方法。那么有沒有什么較好的方法呢?Java其實對多任務調度實現了非常好的封裝在(java.util.concurrent包里),我們可以使用下面代碼方便的實現異步。
先看一下被測類:
import java.util.*; public class TimeOutCall { public String CallWithTimeOut( long timeSetting ) //被測物方法,輸入參數可以設置多長時間返回。 { long startTime = System.currentTimeMillis (); while(true ) { if(System.currentTimeMillis() - startTime < timeSetting ) continue; else return "Result returned!" ; } } }
再看一下實現超時的調用類:
import java.util.concurrent.ExecutorService; import java.util.concurrent.Callable; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; public class CallTest { public static void main(String [] args) { ExecutorService service = Executors. newSingleThreadExecutor(); Callable<String> callable = new Callable<String>(){ //實現Callable接口的匿名類 public String call() throws Exception{ TimeOutCall toc = new TimeOutCall(); return toc.CallWithTimeOut(3000); //3秒返回結果 } }; Future<String> future = service.submit( callable); service.shutdown(); try { if(service.awaitTermination (1000, TimeUnit.MILLISECONDS) == false)//等待1秒后拋出異常。 throw new TimeoutException(); System. out.println(future .get()); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } }
上述代碼中我們創建了一個ExecutorService類,並且將一個Callable接口類型的作業提交給了類,而Future可以異步的等待Callable作業的執行結果。想查看作業執行時間的話,ExecutorService提供了一個相當方便的方法 awaitTermination來檢測是否作業在開始后一段時間還在繼續執行,通過對方法第一個參數的設置,我們很容易能夠設置超時的時限。
事實上,Junit也是用類似的方法實現超時檢測的,在Junit中,我們可以方便的使用Annotation給一個測試方法加入超時檢測:
@Test(timeout =1000) public void testXXX(){ ... }
而它在代碼中對方法超時的實現核心代碼如下(org.junit.internal.runners.MethodRoadie.java中):
private void runWithTimeout(final long timeout) { runBeforesThenTestThenAfters(new Runnable() { public void run() { ExecutorService service = Executors.newSingleThreadExecutor(); Callable<Object> callable = new Callable<Object>() { public Object call() throws Exception { runTestMethod(); return null; } }; Future<Object> result = service.submit(callable); service.shutdown(); try { boolean terminated = service.awaitTermination(timeout, TimeUnit.MILLISECONDS); if (!terminated) { service.shutdownNow(); } result.get(0, TimeUnit.MILLISECONDS); // throws the exception if one occurred during the invocation } catch (TimeoutException e) { addFailure(new TestTimedOutException(timeout, TimeUnit.MILLISECONDS)); } catch (Exception e) { addFailure(e); } } }); }
不過,如果你在Junit中使用了@BeforeClass 和@AfterClass,並且有多個測試用例都必須檢測超時,則建議使用Rules來設置整體超時時間。
大家也可以參考StackOverFlow上的這個鏈接,看一下討論過程,相信會有一個更加深入的理解:
http://stackoverflow.com/questions/2758612/executorservice-that-interrupts-tasks-after-a-timeout
上文說過,不同語言,不同操作系統,timeout實現起來很不一樣。比如python,如果在UnixLike系統下,可以用python內置的signal包來方便的實現超時,我們來看下面的例子:
import signal, os def handler(signum, frame): #產生超時后調用。 print 'Signal handler called with signal', signum raise IOError("Couldn't open device!") # Set the signal handler and a 5-second alarm signal.signal(signal.SIGALRM, handler) #指定handler signal.alarm(5) #設置5秒超時 # This open() may hang indefinitely fd = os.open('/dev/ttyS0', os.O_RDWR) signal.alarm(0)
在使用的時候,我們可以很方便的把它也封裝成Decorator(等同於Java的Annotation)
但是,在Windows下,就沒有那么幸運了,因為signal包直接使用了unixlike操作系統的信號量機制,這時候實現超時就會相對麻煩一些。什么?多線程?答案又一次對了。我們可以使用multiprocessing
包中的函數來實現超時檢測。下面是粗略的代碼實現(真的是粗略的實現)要想深入了解,請看python多線程的在線文檔
__author__ = 'lucasliu' from multiprocessing import Process import time def timefucntion(sleeptime ): time.sleep(sleeptime) print 'timefuction returned after', sleeptime,'seconds' if __name__ == '__main__': p = Process(target=timefucntion,args=(3,)) starttime = time.time() timeoutsetting = 2 p.start() p.join(timeout=timeoutsetting) if abs(time.time() - starttime - timeoutsetting)<0.1: print 'timeout' p.terminate()
那么在常見python的測試框架里,timeout又是如何實現的呢?有點兒遺憾,python的內置單測框架unittest不支持超時檢測。因此我們來看Robotframework是如何實現超時的:一句話,就是根據不同的操作系統,用不同的方法實現超時,並在框架上層統一起來,對用戶透明。源碼量稍微有點兒大,就不在這里搬運了。有興趣可以去看Robotframework的 robot.running.timeouts包里的代碼,看完一定會有收獲。
至於ruby,就封裝的更好了。直接有一個timeout庫,引入后可以極為方便的實現timeout,如下面代碼:如果do sothing的時間超過了 timeoutsetting的設置就會拋出異常。所以,用ruby的同學相對幸福一些。
require 'timeout' begin timeout(timeoutsetting ){ do something } rescue Exception puts "timeout" ensure puts "finish" end