2013-02-20 28 views
1

我想为使用宏中央调度的一些GUI组件编写单元测试。我想从测试中调用线程代码,等待它完成,然后检查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"); 
} 

我的第一个想法是同步调用的东西对相关队列,像这样。不幸的是,这导致从主队列调用时死锁(因为有dispatch_sync()到GUI代码的主队列,并且测试在主线程同时运行):

-(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代码在测试检查结果之前无法保证完成。

感谢您的任何想法!

+0

你不能在另一个线程或队列上运行单元测试吗? – 2013-02-20 23:47:09

回答

2

您应该解耦您的测试需求,以等待GUI更新从主代码路径的运行方式运行。在您发布的第一个代码块中,dispatch_sync几乎肯定是错误的方法(与dispatch_async),因为您将无缘无故地阻止在主线程中等待的后台线程(在dispatch_sync之后没有代码),这可能会导致线程匮乏(在部署中)。我猜你已经做了dispatch_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。L:

#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 

希望这有助于。

+0

谢谢。我采用第二种方法,使用类别。下面是它看起来像现在: – stevel 2013-02-21 20:10:48

+0

我做了NSObject的+ AsyncGUITestSupport方法,它允许测试代码看起来像: '[查看prepareForOperation]' '[查看更新]' '[查看waitForOperationToFinish]' – stevel 2013-02-21 20:18:26

+0

似乎合理;乐意效劳。 – ipmcc 2013-02-22 11:45:38