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

不幸的是,这个“解决方案”存在泵送主运行循环并导致其他测试运行的问题,这对我来说破坏了许多事情。

我也不想将 GUI 代码更改为dispatch_sync()dispatch_async()因为这不是该队列的正确行为,并且在测试中,GUI 代码也不能保证在测试检查结果之前完成。

感谢您的任何想法!

4

1 回答 1

2

您应该将等待 GUI 更新运行的测试需求与主代码路径的运行方式分离。在您发布的第一个代码块中,几乎dispatch_sync可以肯定是错误的方法(与在部署中)。我猜你这样做是为了尝试使用队列本身来联锁两个并行任务。如果您真的致力于使用这种不太理想的方法,您可以执行以下操作:dispatch_asyncdispatch_syncdispatch_sync

- (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 代码非常少。(注意:实际上根本不可能没有入侵,因为为了使两个并行任务互锁,它们必须共享一些要互锁东西——共享的东西——在你现有的代码中它是队列,在这里我'正在使用信号量。)考虑这个人为的例子:我为测试工具添加了一个通用的方法来推入一个信号量,可以用来在后台操作完成时通知它。对要测试的代码的“入侵”仅限于两个宏。

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 回答