Skip to content

Instantly share code, notes, and snippets.

@drance
Last active August 29, 2015 13:57
Show Gist options
  • Save drance/9613950 to your computer and use it in GitHub Desktop.
Save drance/9613950 to your computer and use it in GitHub Desktop.
Got tired of semaphore/runloop boilerplate for async unit tests
- (void)sleepRunLoopForInterval:(NSTimeInterval)interval whileRunningAsynchronousTest:(void (^)(dispatch_semaphore_t semaphore))test {
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
NSAssert((test != NULL), @"Passed a NULL test block");
test(semaphore);
while(dispatch_semaphore_wait(semaphore, DISPATCH_TIME_NOW)) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:interval]];
}
}
- (void)testAsynchronousStuff {
[self sleepRunLoopForInterval:1.0 whileRunningAsynchronousTest:^(dispatch_semaphore_t semaphore) {
[self thatAPIWeNeedToTestWithCompletion:^{
STAssertTrue(something, @"Noes");
dispatch_semaphore_signal(semaphore);
}];
}];
}
- (void)testMoreAsynchronousStuff {
[self sleepRunLoopForInterval:0.2 whileRunningAsynchronousTest:^(dispatch_semaphore_t semaphore) {
[self thatOtherAPIWeNeedToTestWithCompletion:^(id result){
STAssertNotNil(result, @"Noes");
dispatch_semaphore_signal(semaphore);
}]
}
}
@benlings
Copy link

@frankus - what about:

typedef void(^CompletionBlock)(void);

- (void)sleepRunLoopForInterval:(NSTimeInterval)interval whileRunningAsynchronousTest:(void (^)(CompletionBlock done))test {
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    CompletionBlock doneBlock = ^{
        dispatch_semaphore_signal(semaphore);
    }

    NSAssert((test != NULL), @"Passed a NULL test block");
    test(doneBlock);

    while(dispatch_semaphore_wait(semaphore, DISPATCH_TIME_NOW)) {
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:interval]];
    }
}

- (void)testAsynchronousStuff {
    [self sleepRunLoopForInterval:1.0 whileRunningAsynchronousTest:^(CompletionBlock done) {
        [self thatAPIWeNeedToTestWithCompletion:^{
            STAssertTrue(something, @"Noes");
            done();
        }];
    }];
}

(untested code)

@frankus
Copy link

frankus commented Mar 18, 2014

@benlings: I like that.

Here it is as an XCTestCase subclass:

//
//  FMSAsyncTestCase.m
//  TestTest
//
//  Created by Frank Schmitt on 3/18/14.
//

#import "FMSAsyncTestCase.h"
#import <objc/runtime.h>
#import <XCTest/XCTest.h>

@implementation FMSAsyncTestCase

typedef void(^CompletionBlock)(void);

+ (NSArray *)testInvocations {
    NSArray *parentInvocations = [super testInvocations];

    NSMutableArray *additionalInvocations = [NSMutableArray array];
    NSUInteger mc = 0;
    Method * mlist = class_copyMethodList([self class], &mc);
    for (NSUInteger i = 0; i < mc; i ++) {
        SEL selector = method_getName(mlist[i]);
        NSString *selectorName = [NSString stringWithFormat:@"%s", sel_getName(selector)];

        if ([selectorName hasPrefix:@"test"]) {
            NSMethodSignature *sig = [self instanceMethodSignatureForSelector:selector];

            if ([sig numberOfArguments] == 3) {
                NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
                invocation.selector = selector;

                [additionalInvocations addObject:invocation];
            }
        }
    }

    return [parentInvocations arrayByAddingObjectsFromArray:additionalInvocations];
}

- (void)invokeTest {
    if ([[self.invocation methodSignature] numberOfArguments] == 3) {
        [self setUp];

        [self sleepRunLoopForInterval:1.0 whileRunningAsynchronousTest:^(CompletionBlock done){
            [self.invocation setArgument:&done atIndex:2];

            [self.invocation invoke];
        }];

        [self tearDown];
    } else
        [super invokeTest];
}

- (void)sleepRunLoopForInterval:(NSTimeInterval)interval whileRunningAsynchronousTest:(void (^)(CompletionBlock done))test {
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    CompletionBlock doneBlock = ^{
        dispatch_semaphore_signal(semaphore);
    };

    test(doneBlock);

    while(dispatch_semaphore_wait(semaphore, DISPATCH_TIME_NOW)) {
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:interval]];
    }
}

@end

Then you can write tests thusly:

- (void)testWillBeTrue:(CompletionBlock)done {
    [self.myObject willBeTrue:^(BOOL isTrue){
        XCTAssertTrue(isTrue, @"will be true should be true");
        done();
    }];
}

Unfortunately Xcode's introspection of test code doesn't appear to be deep enough to get a little green/red diamond next to passing/failing tests if it's architected this way.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment