64

XcodeにGoogleToolboxfor Macをインストールし、ここにある単体テストを設定する手順に従いました。

それはすべてうまく機能し、すべてのオブジェクトで同期メソッドを完全にテストできます。ただし、実際にテストしたい複雑な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");
}
4

13 に答える 13

52

私は同じ質問に遭遇し、私のために働く別の解決策を見つけました。

次のようにセマフォを使用して、非同期操作を同期フローに変換するための「オールドスクール」アプローチを使用します。

// 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];

その後、デリゲートで。

于 2010-04-11T18:29:30.370 に答える
44

この質問はほぼ1年前に行われ、回答されたことに感謝しますが、与えられた回答に同意せずにはいられません。非同期操作、特にネットワーク操作のテストは非常に一般的な要件であり、正しく行うために重要です。与えられた例では、実際のネットワーク応答に依存している場合、テストの重要な価値の一部が失われます。具体的には、テストは、通信しているサーバーの可用性と機能の正確さに依存するようになります。この依存関係により、テストが行​​われます

  • より壊れやすい(サーバーがダウンした場合はどうなりますか?)
  • 包括的ではありません(障害応答またはネットワークエラーを一貫してテストするにはどうすればよいですか?)
  • これをテストすることを想像すると、かなり遅くなります。

単体テストは、数分の1秒で実行する必要があります。テストを実行するたびに数秒のネットワーク応答を待つ必要がある場合は、テストを頻繁に実行する可能性は低くなります。

単体テストは、主に依存関係のカプセル化に関するものです。テスト対象のコードの観点から、2つのことが起こります。

  1. メソッドは、おそらくNSURLConnectionをインスタンス化することにより、ネットワーク要求を開始します。
  2. 指定したデリゲートは、特定のメソッド呼び出しを介して応答を受け取ります。

デリゲートは、リモートサーバーからの実際の応答からであれ、テストコードからの応答であれ、応答がどこから来たのかを気にしないか、気にしないでください。これを利用して、応答を自分で生成するだけで非同期操作をテストできます。テストははるかに高速に実行され、成功または失敗の応答を確実にテストできます。

これは、使用している実際のWebサービスに対してテストを実行するべきではないということではありませんが、それらは統合テストであり、独自のテストスイートに属しています。そのスイートでの障害は、Webサービスに変更があったか、単にダウンしていることを意味している可能性があります。それらはより壊れやすいので、それらを自動化することは、単体テストを自動化することよりも価値が低い傾向があります。

ネットワーク要求に対する非同期応答のテストを正確に行う方法については、いくつかのオプションがあります。メソッドを直接呼び出すことで、デリゲートを個別にテストできます(たとえば、[someDelegate connection:connection didReceiveResponse:someResponse])。これは多少は機能しますが、少し間違っています。オブジェクトが提供するデリゲートは、特定のNSURLConnectionオブジェクトのデリゲートチェーン内の複数のオブジェクトの1つにすぎない場合があります。デリゲートのメソッドを直接呼び出すと、チェーンのさらに上流にある別のデリゲートによって提供される機能の重要な部分が欠落している可能性があります。より良い代替策として、作成したNSURLConnectionオブジェクトをスタブして、そのデリゲートチェーン全体に応答メッセージを送信させることができます。(他のクラスの中でも)NSURLConnectionを再度開き、これを行うライブラリがあります。https://github.com/pivotal/PivotalCoreKit/blob/master/SpecHelperLib/Extensions/NSURLConnection%2BSpec.m

于 2010-11-14T22:28:16.033 に答える
19

St3fan、あなたは天才です。どうもありがとう!

これは私があなたの提案を使ってそれをした方法です。

「ダウンローダー」は、完了時に起動するメソッド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!");
}

もちろん、実行ループは永久に実行される可能性があります。後で改善します。

于 2010-01-29T15:13:43.563 に答える
16

非同期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しますので、完了後は必ず設定してください。もちろん、必要に応じてヘルパーにタイムアウトを追加することもできます。

于 2012-12-31T09:38:51.613 に答える
8

これは注意が必要です。テストで実行ループを設定し、その実行ループを非同期コードに指定する機能も必要になると思います。そうしないと、コールバックはrunloopで実行されるため、発生しません。

ループ内でrunloopを短時間実行するだけでよいと思います。そして、コールバックに共有ステータス変数を設定させます。または、単にコールバックに実行ループを終了するように依頼することもできます。そうすれば、テストが終了したことがわかります。一定時間後にループをstoppngすることで、タイムアウトをチェックできるはずです。その場合、タイムアウトが発生しました。

私はこれをやったことはありませんが、すぐにやらなければならないと思います。結果を共有してください:-)

于 2010-01-29T14:36:44.927 に答える
6

AFNetworkingやASIHTTPRequestなどのライブラリを使用していて、NSOperation(またはこれらのライブラリのサブクラス)を介してリクエストを管理している場合は、NSOperationQueueを使用してテスト/開発サーバーに対してリクエストを簡単にテストできます。

テスト中:

// create request operation

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

// verify response

これは基本的に、操作が完了するまでrunloopを実行し、すべてのコールバックが通常どおりにバックグラウンドスレッドで発生することを可能にします。

于 2012-05-17T13:15:19.443 に答える
6

@ 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;
        }
    }
于 2012-05-19T20:27:24.783 に答える
3

https://github.com/premosystems/XCAsyncTestCaseを使用すると非常に便利だと思います

XCTestCaseに3つの非常に便利なメソッドを追加します

@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];
}
于 2014-04-08T20:36:17.577 に答える
2

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");
}

はるかにきれいで、メインスレッドをブロックしません。

于 2013-01-10T10:12:45.620 に答える
1

私はこれについてブログエントリを書いたばかりです(実際、これは興味深いトピックだと思ったのでブログを始めました)。最終的にメソッドswizzlingを使用したので、待機せずに任意の引数を使用して完了ハンドラーを呼び出すことができます。これは、単体テストには適しているようです。このようなもの:

- (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. 
}

気になる人のためのブログエントリ。

于 2013-04-15T10:37:33.407 に答える
0

私の答えは、ユニットテストは、概念的には、非同期操作のテストには適していないということです。サーバーへの要求や応答の処理などの非同期操作は、1つのユニットではなく、2つのユニットで発生します。

応答を要求に関連付けるには、2つのユニット間の実行を何らかの方法でブロックするか、グローバルデータを維持する必要があります。実行をブロックすると、プログラムは正常に実行されません。グローバルデータを維持している場合は、それ自体にエラーが含まれている可能性のある無関係な機能が追加されています。どちらのソリューションも単体テストの概念全体に違反しており、アプリケーションに特別なテストコードを挿入する必要があります。単体テストの後も、テストコードをオフにして、昔ながらの「手動」テストを実行する必要があります。ユニットテストに費やされた時間と労力は、少なくとも部分的に無駄になります。

于 2012-05-30T23:30:29.850 に答える
0

私はこれに関するこの記事を見つけました。これはmucです http://dadabeatnik.wordpress.com/2013/09/12/xcode-and-asynchronous-unit-testing/

于 2014-01-28T10:02:42.920 に答える