1

グランド セントラル ディスパッチを使用するいくつかの GUI コンポーネントの単体テストを作成しようとしています。テストからスレッド化されたコードを呼び出し、終了するのを待ってから、GUI オブジェクトで結果を確認したいと思います。

dispatch_queue_t myQueue = dispatch_queue_create();

- (void)refreshGui {
    [self.button setEnabled:NO];
    dispatch_async(myQueue, ^{
        //operation of undetermined length
        sleep(1); 

        dispatch_sync(dispatch_get_main_queue(), ^{
            // GUI stuff that must be on the main thread,
            // I want this to be done before I check results in my tests.
            [self.button setEnabled:YES];
        });
    });
}

私のテストでは、次のようなことをしたいと思っています:

-(void)testRefreshGui {
    [object refreshGui];
    [object blockUntilThreadedOperationIsDone];
    STAssertTrue([object isRefreshedProperly], @"did not refresh");
}

私の最初のアイデアは、このように、関連するキューで何かを同期的に呼び出すことでした。残念ながら、メイン キューから呼び出されるとデッドロックが発生します (GUI コードでメイン キューへの dispatch_sync() があり、テストもメイン スレッドで実行されているため)。

-(void)blockOnQueue:(dispatch_queue_t)q {
    dispatch_sync(q, ^{});
}

ディスパッチ グループを使用するdispatch_group_wait(group, DISPATCH_TIME_FOREVER)と、同じ理由でデッドロックが発生します。

私が思いついたハックソリューションはこれでした:

- (void)waitOnQueue:(dispatch_queue_t)q {
    __block BOOL blocking = YES;
    while (blocking) {
        [NSRunLoop.mainRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:.1]];
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{
            dispatch_sync(q, ^{});
            blocking = NO;
        });
    }
}

残念ながら、この「解決策」には、メインの実行ループをポンピングし、他のテストを実行させるという問題があり、これは私にとって多くのことを壊します。

dispatch_sync()また、GUI コードを変更したくありません。dispatch_async()これは、このキューの正しい動作ではないためです。また、テストでは、テストが結果をチェックする前に GUI コードが完了することが保証されないためです。

アイデアをありがとう!

4

1 に答える 1

2

GUIの更新が実行されるのを待つためのテストの必要性を、メインコードパスの実行方法から切り離す必要があります。あなたが投稿した最初のコードブロックでdispatch_syncは、ほぼ間違いなく間違ったアプローチです(vs. dispatch_async)。理由もなく(後にコードがないdispatch_sync)メインスレッドで待機しているバックグラウンドスレッドをブロックするため、スレッドの枯渇につながる可能性があります(展開中)。dispatch_syncキュー自体を使用して2つの並列タスクをインターロックしようとして作成したと思います。そのやや最適ではないアプローチを使用することに真剣に取り組んでいる場合は、次のようなことを行うことができます。

- (void)testOne
{
    SOAltUpdateView* view = [[SOAltUpdateView alloc] initWithFrame: NSMakeRect(0, 0, 100, 100)];

    STAssertNotNil(view, @"View was nil");
    STAssertEqualObjects(view.color, [NSColor redColor] , @"Initial color was wrong");

    dispatch_queue_t q = dispatch_queue_create("test", 0);
    dispatch_group_t group = dispatch_group_create();
    view.queue = q;


    // Run the operation
    [view update];

    // An operation we can wait on
    dispatch_group_async(group, q, ^{ });

    while (dispatch_group_wait(group, DISPATCH_TIME_NOW))
    {
        CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0, YES);
    }

    STAssertEqualObjects(view.color, [NSColor greenColor] , @"Updated color was wrong");

    view.queue = nil;
    [view release];
    dispatch_release(group);
    dispatch_release(q);
}

それはあなたがすでに持っていたものに最も近いように見えたアプローチでしたが、私はもう少し良い/よりクリーンなものを思いつきました:セマフォはあなたのためにこの連動を行うことができ、少しの努力であなたはあなたに侵入することができます実際のGUIコードはごくわずかです。(注: 2つの並列タスクがインターロックするためには、インターロックするものを共有する必要があるため、侵入まったくないことは事実上不可能です。既存のコードでは、それがキューでした。ここでは、セマフォを使用しています。)この不自然な例を考えてみましょう。バックグラウンド操作が完了したときに通知するために使用できるセマフォをテストハーネスにプッシュするための一般的な手段を追加しました。テストされるコードへの「侵入」は、2つのマクロに制限されています。

NSObject + AsyncGUITestSupport.h:

@interface NSObject (AsyncGUITestSupport)

@property (nonatomic, readwrite, assign) dispatch_semaphore_t testCompletionSemaphore;

@end

#define OPERATION_BEGIN(...) do { dispatch_semaphore_t s = self.testCompletionSemaphore; if (s) dispatch_semaphore_wait(s, DISPATCH_TIME_NOW); } while(0)
#define OPERATION_END(...) do { dispatch_semaphore_t s = self.testCompletionSemaphore; if (s) dispatch_semaphore_signal(s); } while(0)

NSObject + AsyncGUITestSupport.m:

#import "NSObject+AsyncGUITestSupport.h"
#import <objc/runtime.h>

@implementation NSObject (AsyncGUITestSupport)

static void * const kTestingSemaphoreAssociatedStorageKey = (void*)&kTestingSemaphoreAssociatedStorageKey;

- (void)setTestCompletionSemaphore:(dispatch_semaphore_t)myProperty
{
    objc_setAssociatedObject(self, kTestingSemaphoreAssociatedStorageKey, myProperty, OBJC_ASSOCIATION_ASSIGN);
}

- (dispatch_semaphore_t)testCompletionSemaphore
{
    return objc_getAssociatedObject(self, kTestingSemaphoreAssociatedStorageKey);
}

@end

SOUpdateView.h

@interface SOUpdateView : NSView
@property (nonatomic, readonly, retain) NSColor* color;
- (void)update;
@end

SOUpdateView.m

#import "SOUpdateView.h"
#import "NSObject+AsyncGUITestSupport.h"

@implementation SOUpdateView
{
    NSUInteger _count;
}

- (NSColor *)color
{
    NSArray* colors = @[ [NSColor redColor], [NSColor greenColor], [NSColor blueColor] ];
    @synchronized(self)
    {
        return colors[_count % colors.count];
    }
}

- (void)drawRect:(NSRect)dirtyRect
{
    [self.color set];
    NSRectFill(dirtyRect);
}

- (void)update
{
    OPERATION_BEGIN();
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(1);

        @synchronized(self)
        {
            _count++;
        }

        dispatch_async(dispatch_get_main_queue(), ^{
            [self setNeedsDisplay: YES];
            OPERATION_END();
        });
    });
}

@end

そして、テストハーネス:

#import "TestSOTestGUI.h"
#import "SOUpdateView.h"
#import "NSObject+AsyncGUITestSupport.h"

@implementation TestSOTestGUI

- (void)testOne
{
    SOUpdateView* view = [[SOUpdateView alloc] initWithFrame: NSMakeRect(0, 0, 100, 100)];

    STAssertNotNil(view, @"View was nil");
    STAssertEqualObjects(view.color, [NSColor redColor] , @"Initial color was wrong");

    // Push in a semaphore...
    dispatch_semaphore_t sem = dispatch_semaphore_create(0);
    view.testCompletionSemaphore = sem;

    // Run the operation
    [view update];

    // Wait for the operation to finish.
    while (dispatch_semaphore_wait(sem, DISPATCH_TIME_NOW))
    {
        CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0, YES);
    }

    // Clear out the semaphore
    view.testCompletionSemaphore = nil;

    STAssertEqualObjects(view.color, [NSColor greenColor] , @"Updated color was wrong");    
}

@end

お役に立てれば。

于 2013-02-21T15:20:20.203 に答える