Skip to main content
deleted 2484 characters in body
Source Link
bizz84
  • 2.2k
  • 24
  • 38

I implemented the solution proposed by Thomas Tempelmann and overall it works greatfine for me.

However, if there's a bug it may be that the delegatethere is not called bya gotcha. Suppose the unit to be tested, resulting in this line not being called contains the following code:

dispatch_async(dispatch_get_main_queue(), ^{
    [self.theLock unlockWithConditionperformSelector:1];selector withObject:nil afterDelay:1.0];
});

This can stall the execution of all tests which is a no-no in a CI environment.

To solve this, I came up with the solution of creating an external interface responsible for lockingThe selector may never be called as we told the NSConditionLock, and unlocking it on a separatemain thread after a given timeout.

Interface fileto lock until the test completes:

#import <Foundation/Foundation.h>

/**
 * Interface providing a lock to be used to transform asynchronous tests into synchronous ones
 * Tests should instance this lock and wait on condition before resuming execution[testBase.
 * This lock is guaranteed to be unlocked automatically after a given timeout if the test doesn't complete,
 * so that tests execution is not stalled
 */

@interface AsyncTestBase lockWhenCondition: NSObject

@property NSConditionLock *lock;

@end1];

Implementation fileOverall, we could get rid of the NSConditionLock altogether and simply use the GHAsyncTestCase class instead.

This is how I use it in my code:

#import "AsyncTestBase.h"

// Uncomment this to avoid thread unlock durring debugging
//#define BREAKPOINTS_ENABLED

#define kLongDelay 5.0f

@implementation AsyncTestBase {
    NSThread *thread;
}

@synthesize lock =@interface _lock;


-NumericTestTests (void)setLock:(NSConditionLock *)lock {
    
    _lock = lock;
    
#ifndef BREAKPOINTS_ENABLED
    [self startThread];
#endif
}

- (NSConditionLock *)lock {
    return _lock;
}


- (void)startThreadGHAsyncTestCase {
    thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadMain) object:nil];
    [thread start];
}

- (void)stopThread {
    
    NSLog(@"%s *** TIMER EXPIRED ***", __func__);
    if (_lock != nil) {
        @end
        [_lock unlockWithCondition:1];
   @implementation }
}

-NumericTestTests (void)threadMain
{
    // Add selector to prevent CFRunLoopRunInMode from returning immediately
    [self performSelector:@selector(stopThread) withObject:nil afterDelay:kLongDelay];
    BOOL done = NO;
    
    do
    {
        // Start the run loop but return after each source is handled.
        SInt32    result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, YES);
        
        // If a source explicitly stopped the run loop, or if there are no
        // sources or timers, go ahead and exit.
        if ((result == kCFRunLoopRunStopped) || (result == kCFRunLoopRunFinished))
            done = YES;
    }
    while (!done);passed;
}
 
@end

The unit test can then call:

- (void)setUp
{
    testBasepassed = [AsyncTestBase new];NO;
}

 

- (void)testMe {
    
    // create the semaphore and lock it once before we start
    // the async operation
    NSConditionLock *tl = [NSConditionLock new];
    testBase.lock = tl;
    
    __block BOOL passed =[self NO;prepare];
    
    NumericTestMyTest *test = [NumericTest[MyTest new];
    
// Run test asynchronously
    [test runTestrun: ^(NSError *error, double value) {
        
        passed = YES;
        
        [self->testBase.lock unlockWithConditionnotify:1];kGHUnitWaitStatusSuccess];
    }];
    
   [test runTest:fakeTest];
    // now lock the semaphore - which will block this thread until
    // [self.theLock unlockWithConditionwaitForStatus:1] gets invoked
    [testBase.lockkGHUnitWaitStatusSuccess lockWhenConditiontimeout:1];
    
    // make sure the async callback did in fact happen by5.0];
    // checking whether it modified a variable
    GHAssertTrue(passed, @"Completion handler not called");
    
    testBase.lock = nil;

}

Much cleaner and doesn't block the main thread.

I implemented the solution proposed by Thomas Tempelmann and it works great for me.

However, if there's a bug it may be that the delegate is not called by the unit to be tested, resulting in this line not being called:

[self.theLock unlockWithCondition:1];

This can stall the execution of all tests which is a no-no in a CI environment.

To solve this, I came up with the solution of creating an external interface responsible for locking the NSConditionLock, and unlocking it on a separate thread after a given timeout.

Interface file:

#import <Foundation/Foundation.h>

/**
 * Interface providing a lock to be used to transform asynchronous tests into synchronous ones
 * Tests should instance this lock and wait on condition before resuming execution.
 * This lock is guaranteed to be unlocked automatically after a given timeout if the test doesn't complete,
 * so that tests execution is not stalled
 */

@interface AsyncTestBase : NSObject

@property NSConditionLock *lock;

@end

Implementation file:

#import "AsyncTestBase.h"

// Uncomment this to avoid thread unlock durring debugging
//#define BREAKPOINTS_ENABLED

#define kLongDelay 5.0f

@implementation AsyncTestBase {
    NSThread *thread;
}

@synthesize lock = _lock;


- (void)setLock:(NSConditionLock *)lock {
    
    _lock = lock;
    
#ifndef BREAKPOINTS_ENABLED
    [self startThread];
#endif
}

- (NSConditionLock *)lock {
    return _lock;
}


- (void)startThread {
    thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadMain) object:nil];
    [thread start];
}

- (void)stopThread {
    
    NSLog(@"%s *** TIMER EXPIRED ***", __func__);
    if (_lock != nil) {
        
        [_lock unlockWithCondition:1];
    }
}

- (void)threadMain
{
    // Add selector to prevent CFRunLoopRunInMode from returning immediately
    [self performSelector:@selector(stopThread) withObject:nil afterDelay:kLongDelay];
    BOOL done = NO;
    
    do
    {
        // Start the run loop but return after each source is handled.
        SInt32    result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, YES);
        
        // If a source explicitly stopped the run loop, or if there are no
        // sources or timers, go ahead and exit.
        if ((result == kCFRunLoopRunStopped) || (result == kCFRunLoopRunFinished))
            done = YES;
    }
    while (!done);
}
 
@end

The unit test can then call:

- (void)setUp
{
    testBase = [AsyncTestBase new];
}

 

- (void)testMe {
    
    // create the semaphore and lock it once before we start
    // the async operation
    NSConditionLock *tl = [NSConditionLock new];
    testBase.lock = tl;
    
    __block BOOL passed = NO;
    
    NumericTest *test = [NumericTest new];
    
// Run test asynchronously
    [test runTest:^(NSError *error, double value) {
        
        passed = YES;
        
        [self->testBase.lock unlockWithCondition:1];
    }];
    
    
    // now lock the semaphore - which will block this thread until
    // [self.theLock unlockWithCondition:1] gets invoked
    [testBase.lock lockWhenCondition:1];
    
    // make sure the async callback did in fact happen by
    // checking whether it modified a variable
    GHAssertTrue(passed, @"Completion handler not called");
    
    testBase.lock = nil;

}

I implemented the solution proposed by Thomas Tempelmann and overall it works fine for me.

However, there is a gotcha. Suppose the unit to be tested contains the following code:

dispatch_async(dispatch_get_main_queue(), ^{
    [self performSelector:selector withObject:nil afterDelay:1.0];
});

The selector may never be called as we told the main thread to lock until the test completes:

[testBase.lock lockWhenCondition:1];

Overall, we could get rid of the NSConditionLock altogether and simply use the GHAsyncTestCase class instead.

This is how I use it in my code:

@interface NumericTestTests : GHAsyncTestCase { }

@end

@implementation NumericTestTests {
    BOOL passed;
}

- (void)setUp
{
    passed = NO;
}

- (void)testMe {
    
    [self prepare];
    
    MyTest *test = [MyTest new];
    [test run: ^(NSError *error, double value) {
        passed = YES;
        [self notify:kGHUnitWaitStatusSuccess];
    }];
    [test runTest:fakeTest];
    
    [self waitForStatus:kGHUnitWaitStatusSuccess timeout:5.0];
    
    GHAssertTrue(passed, @"Completion handler not called");
}

Much cleaner and doesn't block the main thread.

Source Link
bizz84
  • 2.2k
  • 24
  • 38

I implemented the solution proposed by Thomas Tempelmann and it works great for me.

However, if there's a bug it may be that the delegate is not called by the unit to be tested, resulting in this line not being called:

[self.theLock unlockWithCondition:1];

This can stall the execution of all tests which is a no-no in a CI environment.

To solve this, I came up with the solution of creating an external interface responsible for locking the NSConditionLock, and unlocking it on a separate thread after a given timeout.

Interface file:

#import <Foundation/Foundation.h>

/**
 * Interface providing a lock to be used to transform asynchronous tests into synchronous ones
 * Tests should instance this lock and wait on condition before resuming execution.
 * This lock is guaranteed to be unlocked automatically after a given timeout if the test doesn't complete,
 * so that tests execution is not stalled
 */

@interface AsyncTestBase : NSObject

@property NSConditionLock *lock;

@end

Implementation file:

#import "AsyncTestBase.h"

// Uncomment this to avoid thread unlock durring debugging
//#define BREAKPOINTS_ENABLED

#define kLongDelay 5.0f

@implementation AsyncTestBase {
    NSThread *thread;
}

@synthesize lock = _lock;


- (void)setLock:(NSConditionLock *)lock {
    
    _lock = lock;
    
#ifndef BREAKPOINTS_ENABLED
    [self startThread];
#endif
}

- (NSConditionLock *)lock {
    return _lock;
}


- (void)startThread {
    thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadMain) object:nil];
    [thread start];
}

- (void)stopThread {
    
    NSLog(@"%s *** TIMER EXPIRED ***", __func__);
    if (_lock != nil) {
        
        [_lock unlockWithCondition:1];
    }
}

- (void)threadMain
{
    // Add selector to prevent CFRunLoopRunInMode from returning immediately
    [self performSelector:@selector(stopThread) withObject:nil afterDelay:kLongDelay];
    BOOL done = NO;
    
    do
    {
        // Start the run loop but return after each source is handled.
        SInt32    result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, YES);
        
        // If a source explicitly stopped the run loop, or if there are no
        // sources or timers, go ahead and exit.
        if ((result == kCFRunLoopRunStopped) || (result == kCFRunLoopRunFinished))
            done = YES;
    }
    while (!done);
}

@end

The unit test can then call:

- (void)setUp
{
    testBase = [AsyncTestBase new];
}



- (void)testMe {
    
    // create the semaphore and lock it once before we start
    // the async operation
    NSConditionLock *tl = [NSConditionLock new];
    testBase.lock = tl;
    
    __block BOOL passed = NO;
    
    NumericTest *test = [NumericTest new];
    
// Run test asynchronously
    [test runTest:^(NSError *error, double value) {
        
        passed = YES;
        
        [self->testBase.lock unlockWithCondition:1];
    }];
    
    
    // now lock the semaphore - which will block this thread until
    // [self.theLock unlockWithCondition:1] gets invoked
    [testBase.lock lockWhenCondition:1];
    
    // make sure the async callback did in fact happen by
    // checking whether it modified a variable
    GHAssertTrue(passed, @"Completion handler not called");
    
    testBase.lock = nil;

}