簡體   English   中英

如何對異步API進行單元測試?

[英]How to unit test asynchronous APIs?

我已將Google Toolbox for Mac安裝到Xcode中,並按照說明在此處設置單元測試。

這一切都很好,我可以在我的所有對象上測試我的同步方法。 但是,我實際想要測試的大多數復雜API通過調用委托上的方法異步返回結果 - 例如,對文件下載和更新系統的調用將立即返回,然后在文件完成下載時運行-fileDownloadDidComplete:方法。

我如何將其作為單元測試進行測試?

好像我想要testDownload函數,或者至少要測試框架'等待'fileDownloadDidComplete:方法來運行。

編輯:我現在已經切換到使用XCode內置XCTest系統,並發現Github上的TVRSMonitor提供了一種簡單的方法來使用信號量等待異步操作完成。

例如:

- (void)testLogin {
  TRVSMonitor *monitor = [TRVSMonitor monitor];
  __block NSString *theToken;

  [[Server instance] loginWithUsername:@"foo" password:@"bar"
                               success:^(NSString *token) {
                                   theToken = token;
                                   [monitor signal];
                               }

                               failure:^(NSError *error) {
                                   [monitor signal];
                               }];

  [monitor wait];

  XCTAssert(theToken, @"Getting token");
}

我遇到了同樣的問題,發現了一個適合我的不同解決方案。

我使用“舊學校”方法通過使用信號量將異步操作轉換為同步流,如下所示:

// create the object that will perform an async operation
MyConnection *conn = [MyConnection new];
STAssertNotNil (conn, @"MyConnection init failed");

// create the semaphore and lock it once before we start
// the async operation
NSConditionLock *tl = [NSConditionLock new];
self.theLock = tl;
[tl release];    

// start the async operation
self.testState = 0;
[conn doItAsyncWithDelegate:self];

// now lock the semaphore - which will block this thread until
// [self.theLock unlockWithCondition:1] gets invoked
[self.theLock lockWhenCondition:1];

// make sure the async callback did in fact happen by
// checking whether it modified a variable
STAssertTrue (self.testState != 0, @"delegate did not get called");

// we're done
[self.theLock release]; self.theLock = nil;
[conn release];

一定要調用

[self.theLock unlockWithCondition:1];

然后在代表中。

我很欣賞這個問題在一年前被提出並得到了回答,但我不禁對這些問題表示不同意見。 測試異步操作,特別是網絡操作,是一個非常常見的要求,並且對於正確的操作非常重要。 在給定的示例中,如果您依賴於實際的網絡響應,則會丟失測試的一些重要值。 具體來說,您的測試取決於您正在與之通信的服務器的可用性和功能正確性; 這種依賴會使你的測試

  • 更脆弱(如果服務器出現故障會怎么樣?)
  • 不太全面(如何一致地測試故障響應或網絡錯誤?)
  • 想象測試這個顯着慢:

單元測試應該在幾分之一秒內完成。 如果每次運行測試時都必須等待多秒的網絡響應,那么您不太可能經常運行它們。

單元測試主要是關於封裝依賴關系; 從您測試的代碼的角度來看,有兩件事情發生:

  1. 您的方法可能通過實例化NSURLConnection來啟動網絡請求。
  2. 您指定的委托通過某些方法調用接收響應。

您的代表不會或不應該關注響應的來源,無論是來自遠程服務器的實際響應還是來自您的測試代碼。 您可以通過自己簡單地生成響應來利用此功能來測試異步操作。 您的測試運行得更快,您可以可靠地測試成功或失敗響應。

這並不是說您不應該針對您正在使用的真實Web服務運行測試,而是那些是集成測試並且屬於他們自己的測試套件。 該套件中的失敗可能意味着Web服務發生了變化,或者只是簡單地失敗了。 由於它們更脆弱,因此自動化它們的價值往往低於自動化單元測試。

關於如何測試對網絡請求的異步響應,您有幾個選擇。 您可以通過直接調用方法來單獨測試委托(例如[someDelegate connection:connection didReceiveResponse:someResponse])。 這會有所作為,但稍有不妥。 您的對象提供的委托可能只是特定NSURLConnection對象的委托鏈中的多個對象之一; 如果你直接調用你的代理人的方法,你可能會錯過由鏈上的另一個代表提供的一些關鍵功能。 作為更好的替代方法,您可以存根您創建的NSURLConnection對象,並讓它將響應消息發送到其整個委托鏈。 有些庫將重新打開NSURLConnection(以及其他類)並為您執行此操作。 例如: https//github.com/pivotal/PivotalCoreKit/blob/master/SpecHelperLib/Extensions/NSURLConnection%2BSpec.m

St3fan,你是個天才。 非常感謝!

這就是我用你的建議做到的。

'Downloader'使用方法DownloadDidComplete定義一個協議,該方法在完成時觸發。 有一個BOOL成員變量'downloadComplete',用於終止運行循環。

-(void) testDownloader {
 downloadComplete = NO;
 Downloader* downloader = [[Downloader alloc] init] delegate:self];

 // ... irrelevant downloader setup code removed ...

 NSRunLoop *theRL = [NSRunLoop currentRunLoop];

 // Begin a run loop terminated when the downloadComplete it set to true
 while (!downloadComplete && [theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);

}


-(void) DownloaderDidComplete:(Downloader*) downloader withErrors:(int) errors {
    downloadComplete = YES;

    STAssertNotEquals(errors, 0, @"There were errors downloading!");
}

當然,運行循環可能會永遠運行。我稍后會改進它!

我寫了一個小助手,可以很容易地測試異步API。 首先是幫手:

static inline void hxRunInMainLoop(void(^block)(BOOL *done)) {
    __block BOOL done = NO;
    block(&done);
    while (!done) {
        [[NSRunLoop mainRunLoop] runUntilDate:
            [NSDate dateWithTimeIntervalSinceNow:.1]];
    }
}

你可以像這樣使用它:

hxRunInMainLoop(^(BOOL *done) {
    [MyAsyncThingWithBlock block:^() {
        /* Your test conditions */
        *done = YES;
    }];
});

它只會在done變為TRUE ,因此請務必在完成后設置它。 當然,如果你願意,可以給助手添加一個超時,

這很棘手。 我認為你需要在測試中設置一個runloop,並且能夠為你的異步代碼指定runloop。 否則回調將不會發生,因為它們是在runloop上執行的。

我猜你可以在一個循環中短時間運行runloop。 讓回調設置一些共享狀態變量。 或者甚至可以簡單地要求回調終止runloop。 那樣你就知道測試結束了。 您應該能夠通過在一段時間后停止循環來檢查超時。 如果發生這種情況,則會發生超時。

我從來沒有這樣做,但我想不久就會想到。 請分享你的結果:-)

如果您正在使用AFNetworking或ASIHTTPRequest等庫並通過NSOperation(或具有這些庫的子類)管理您的請求,那么可以使用NSOperationQueue對測試/ dev服務器進行測試:

在測試中:

// create request operation

NSOperationQueue* queue = [[NSOperationQueue alloc] init];
[queue addOperation:request];
[queue waitUntilAllOperationsAreFinished];

// verify response

這基本上運行runloop直到操作完成,允許所有回調在正常情況下在后台線程上發生。

要詳細說明@ St3fan的解決方案,您可以在發起請求后嘗試:

- (BOOL)waitForCompletion:(NSTimeInterval)timeoutSecs
{
    NSDate *timeoutDate = [NSDate dateWithTimeIntervalSinceNow:timeoutSecs];

    do
    {
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:timeoutDate];
        if ([timeoutDate timeIntervalSinceNow] < 0.0)
        {
            break;
        }
    }
    while (!done);

    return done;
}

其他方式:

//block the thread in 0.1 second increment, until one of callbacks is received.
    NSRunLoop *theRL = [NSRunLoop currentRunLoop];

    //setup timeout
    float waitIncrement = 0.1f;
    int timeoutCounter  = (int)(30 / waitIncrement); //30 sec timeout
    BOOL controlConditionReached = NO;


    // Begin a run loop terminated when the downloadComplete it set to true
    while (controlConditionReached == NO)
    {

        [theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:waitIncrement]];
        //control condition is set in one of your async operation delegate methods or blocks
        controlConditionReached = self.downloadComplete || self.downloadFailed ;

        //if there's no response - timeout after some time
        if(--timeoutCounter <= 0)
        {
            break;
        }
    }

我發現使用https://github.com/premosystems/XCAsyncTestCase非常方便

它為XCTestCase添加了三個非常方便的方法

@interface XCTestCase (AsyncTesting)

- (void)waitForStatus:(XCTAsyncTestCaseStatus)status timeout:(NSTimeInterval)timeout;
- (void)waitForTimeout:(NSTimeInterval)timeout;
- (void)notify:(XCTAsyncTestCaseStatus)status;

@end

允許非常干凈的測試。 項目本身的一個例子:

- (void)testAsyncWithDelegate
{
    NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.google.com"]];
    [NSURLConnection connectionWithRequest:request delegate:self];
    [self waitForStatus:XCTAsyncTestCaseStatusSucceeded timeout:10.0];
}

- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
    NSLog(@"Request Finished!");
    [self notify:XCTAsyncTestCaseStatusSucceeded];
}

- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
    NSLog(@"Request failed with error: %@", error);
    [self notify:XCTAsyncTestCaseStatusFailed];
}

我實施了Thomas Tempelmann提出的解決方案,總體而言,它對我來說很好。

但是,有一個問題。 假設要測試的單元包含以下代碼:

dispatch_async(dispatch_get_main_queue(), ^{
    [self performSelector:selector withObject:nil afterDelay:1.0];
});

可能永遠不會調用選擇器,因為我們告訴主線程在測試完成之前鎖定:

[testBase.lock lockWhenCondition:1];

總的來說,我們可以完全擺脫NSConditionLock並簡單地使用GHAsyncTestCase類。

這是我在我的代碼中使用它的方式:

@interface NumericTestTests : GHAsyncTestCase { }

@end

@implementation NumericTestTests {
    BOOL passed;
}

- (void)setUp
{
    passed = NO;
}

- (void)testMe {

    [self prepare];

    MyTest *test = [MyTest new];
    [test run: ^(NSError *error, double value) {
        passed = YES;
        [self notify:kGHUnitWaitStatusSuccess];
    }];
    [test runTest:fakeTest];

    [self waitForStatus:kGHUnitWaitStatusSuccess timeout:5.0];

    GHAssertTrue(passed, @"Completion handler not called");
}

更干凈,並沒有阻止主線程。

我剛剛寫了一篇關於此的博客文章(事實上我開了一個博客,因為我認為這是一個有趣的話題)。 我最終使用方法調配,所以我可以使用我想要的任何參數調用完成處理程序而無需等待,這對於單元測試似乎很好。 像這樣的東西:

- (void)swizzledGeocodeAddressString:(NSString *)addressString completionHandler:(CLGeocodeCompletionHandler)completionHandler
{
    completionHandler(nil, nil); //You can test various arguments for the handler here.
}

- (void)testGeocodeFlagsComplete
{
    //Swizzle the geocodeAddressString with our own method.
    Method originalMethod = class_getInstanceMethod([CLGeocoder class], @selector(geocodeAddressString:completionHandler:));
    Method swizzleMethod = class_getInstanceMethod([self class], @selector(swizzledGeocodeAddressString:completionHandler:));
    method_exchangeImplementations(originalMethod, swizzleMethod);

    MyGeocoder * myGeocoder = [[MyGeocoder alloc] init];
    [myGeocoder geocodeAddress]; //the completion handler is called synchronously in here.

    //Deswizzle the methods!
    method_exchangeImplementations(swizzleMethod, originalMethod);

    STAssertTrue(myGeocoder.geocoded, @"Should flag as geocoded when complete.");//You can test the completion handler code here. 
}

任何關心的人的博客條目

我的答案是,概念上,單元測試不適合測試異步操作。 異步操作(例如對服務器的請求和響應的處理)不是在一個單元中發生,而是在兩個單元中發生。

要將響應與請求相關聯,您必須以某種方式阻止兩個單元之間的執行,或者維護全局數據。 如果阻止執行,那么您的程序沒有正常執行,如果您維護全局數據,則添加了可能本身包含錯誤的無關功能。 這兩種解決方案都違反了單元測試的整體思路,並要求您在應用程序中插入特殊的測試代碼; 然后在進行單元測試后,您仍然需要關閉測試代碼並進行老式的“手動”測試。 然后,花在單元測試上的時間和精力至少部分地被浪費了。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM