"Timeout"在測試框架里是如何被實現的


        今天組里的小伙伴問了我一個問題:“我這里有一個底層驅動的接口,我想在測試它的時候加上超時限制,時間一過就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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM