32

これは私の前の質問に関連していますが、私がそれを新しい質問に投げ込むと思ったほど十分に異なっています。カスタムキューで非同期を実行し、完了時にメインスレッドで完了ブロックを実行するコードがあります。この方法を中心に単体テストを書きたいと思います。私の方法はMyObject次のようになります。

+ (void)doSomethingAsyncThenRunCompletionBlockOnMainQueue:(void (^)())completionBlock {

    dispatch_queue_t customQueue = dispatch_queue_create("com.myObject.myCustomQueue", 0);

    dispatch_async(customQueue, ^(void) {

        dispatch_queue_t currentQueue = dispatch_get_current_queue();
        dispatch_queue_t mainQueue = dispatch_get_main_queue();

        if (currentQueue == mainQueue) {
            NSLog(@"already on main thread");
            completionBlock();
        } else {
            dispatch_async(mainQueue, ^(void) {
                NSLog(@"NOT already on main thread");
                completionBlock();
        }); 
    }
});

}

安全性を高めるためにメインキューテストを投入しましたが、常にヒットしdispatch_asyncます。私の単体テストは次のようになります。

- (void)testDoSomething {

    dispatch_semaphore_t sema = dispatch_semaphore_create(0);

    void (^completionBlock)(void) = ^(void){        
        NSLog(@"Completion Block!");
        dispatch_semaphore_signal(sema);
    }; 

    [MyObject doSomethingAsyncThenRunCompletionBlockOnMainQueue:completionBlock];

    // Wait for async code to finish
    dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
    dispatch_release(sema);

    STFail(@"I know this will fail, thanks");
}

非同期コードが完了する前にテストが終了するのをブロックするために、セマフォを作成します。これは、メインスレッドで実行するために完了ブロックを必要としない場合にうまく機能します。ただし、上記のリンク先の質問で2人の人が指摘したように、テストがメインスレッドで実行されてから、メインスレッドの完了ブロックをキューに入れるという事実は、永遠にハングすることを意味します。

非同期キューからメインキューを呼び出すことは、UIなどを更新するためによく見かけるパターンです。メインキューにコールバックする非同期コードをテストするためのより良いパターンを持っている人はいますか?

4

6 に答える 6

58

ブロックをメインキューにディスパッチして実行するには、2つの方法があります。dispatch_mainDrewsmitsが述べたように、最初は経由です。ただし、彼も指摘したようdispatch_mainに、テストでの使用には大きな問題があります。それは決して戻りません。それはただそこに座って、永遠の残りのためにやってくるブロックを実行するのを待っています。ご想像のとおり、これは単体テストにはあまり役立ちません。

幸いなことに、別のオプションがあります。マニュアルページCOMPATIBILITYセクションには、次のように書かれています。dispatch_main

Cocoaアプリケーションはdispatch_main()を呼び出す必要はありません。メインキューに送信されたブロックは、アプリケーションのメインNSRunLoopまたはCFRunLoopの「コモンモード」の一部として実行されます。

つまり、Cocoaアプリを使用している場合、ディスパッチキューはメインスレッドのによって排出されますNSRunLoop。したがって、テストが終了するのを待っている間、実行ループを実行し続けるだけです。次のようになります。

- (void)testDoSomething {

    __block BOOL hasCalledBack = NO;

    void (^completionBlock)(void) = ^(void){        
        NSLog(@"Completion Block!");
        hasCalledBack = YES;
    }; 

    [MyObject doSomethingAsyncThenRunCompletionBlockOnMainQueue:completionBlock];

    // Repeatedly process events in the run loop until we see the callback run.

    // This code will wait for up to 10 seconds for something to come through
    // on the main queue before it times out. If your tests need longer than
    // that, bump up the time limit. Giving it a timeout like this means your
    // tests won't hang indefinitely. 

    // -[NSRunLoop runMode:beforeDate:] always processes exactly one event or
    // returns after timing out. 

    NSDate *loopUntil = [NSDate dateWithTimeIntervalSinceNow:10];
    while (hasCalledBack == NO && [loopUntil timeIntervalSinceNow] > 0) {
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
                                 beforeDate:loopUntil];
    }

    if (!hasCalledBack)
    {
        STFail(@"I know this will fail, thanks");
    }
}
于 2011-10-20T00:21:13.847 に答える
18

セマフォとランループチャーニングを使用する別の方法。dispatch_semaphore_waitは、タイムアウトした場合にゼロ以外を返すことに注意してください。

- (void)testFetchSources
{
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

    [MyObject doSomethingAsynchronousWhenDone:^(BOOL success) {
        STAssertTrue(success, @"Failed to do the thing!");
        dispatch_semaphore_signal(semaphore);
    }];

    while (dispatch_semaphore_wait(semaphore, DISPATCH_TIME_NOW))
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
                                 beforeDate:[NSDate dateWithTimeIntervalSinceNow:10]];

    dispatch_release(semaphore);
}
于 2011-12-13T23:59:48.690 に答える
7

Squareは、これを簡単にするSocketRocketプロジェクトにSenTestCaseへの巧妙な追加を含めました。あなたはそれをこのように呼ぶことができます:

[self runCurrentRunLoopUntilTestPasses:^BOOL{
    return [someOperation isDone];
} timeout: 60 * 60];

コードはここから入手できます:

SenTestCase + SRTAdditions.h

SenTestCase + SRTAdditions.m

于 2012-12-02T23:07:13.567 に答える
7

BJ Homerのソリューションは、これまでのところ最良のソリューションです。そのソリューションに基づいて構築されたマクロをいくつか作成しました。

ここでプロジェクトをチェックしてくださいhttps://github.com/hfossli/AGAsyncTestHelper

- (void)testDoSomething {

    __block BOOL somethingIsDone = NO;

    void (^completionBlock)(void) = ^(void){        
        NSLog(@"Completion Block!");
        somethingIsDone = YES;
    }; 

    [MyObject doSomethingAsyncThenRunCompletionBlockOnMainQueue:completionBlock];

    WAIT_WHILE(!somethingIsDone, 1.0); 
    NSLog(@"This won't be reached until async job is done");
}

-macroWAIT_WHILE(expressionIsTrue, seconds)は、式が真でなくなるか、制限時間に達するまで、入力を評価します。これ以上きれいにするのは難しいと思います

于 2013-07-26T09:49:09.103 に答える
3

dispatch_main()メインキューでブロックを実行する最も簡単な方法は、メインスレッドから呼び出すことです。ただし、ドキュメントからわかる限り、それは二度と戻らないので、テストが失敗したかどうかはわかりません。

もう1つのアプローチは、ディスパッチ後にユニットテストを実行ループに入れることです。次に、完了ブロックを実行する機会があり、実行ループがタイムアウトする機会もあります。その後、完了ブロックが実行されていない場合は、テストが失敗したと見なすことができます。

于 2011-10-19T08:28:50.290 に答える
2

この質問に対する他のいくつかの回答に基づいて、私はこれを便利な(そして楽しい)ために設定しました:https ://github.com/kallewoof/UTAsync

それが誰かを助けることを願っています。

于 2013-06-29T21:01:11.967 に答える