web-dev-qa-db-ja.com

完了時にメインキューを呼び出す非同期キューをユニットテストするためのパターン

これは私の前の 質問 に関連していますが、私がそれを新しいものに投げ込むと思ったほど十分に異なっています。カスタムキューで非同期を実行し、完了時にメインスレッドで完了ブロックを実行するコードがいくつかあります。この方法を中心に単体テストを書きたいと思います。 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");
}

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

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

32
Drewsmits

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

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

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

セマフォとランループチャーニングを使用する別の方法。 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);
}
18
qwzybug

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

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

7
hfossli

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

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

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

SenTestCase + SRTAdditions.h

SenTestCase + SRTAdditions.m

7
dstnbrkr

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

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

3
JeremyP

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

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

2
Kalle