2013-02-20 22 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