Question

I'm struggling to figure out the best method to test interacting with Core Data in a background thread. I have the following class method:

+ (void)fetchSomeJSON
{
    // Download some json then parse it in the block
    [[AFHTTPClient sharedClient] fetchAllThingsWithCompletion:^(id results, NSError *error) {
        if ([results count] > 0) {

            NSManagedObjectContext *backgroundContext = //... create a new context for background insertion
            dispatch_queue_t background = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);

            dispatch_async(background, ^{ // If I comment this out, my test runs just fine

                //... insert and update some entities
                for (NSString *str in results) {
                    NSManagedObject *object = //...
                }
            });
        }
    }];
}

I'm currently testing this method with the following Kiwi code:

describe(@"MyAction", ^{

    __block void (^completionBlock)(NSArray *array, NSError *error);

    beforeEach(^{
        // Stub the http client
        id mockClient = [AFHTTPClient mock];
        [WRNAPIClient stub:@selector(sharedClient) andReturn:mockClient];

        // capture the block argument
        KWCaptureSpy *spy = [mockClient captureArgument:@selector(fetchAllThingsWithCompletion:) atIndex:0];
        [MyClass fetchSomeJSON]; // Call the method so we can capture the block
        completionBlock = spy.argument;

        // run the completion block 
        completionBlock(@[@"blah"], nil);
    })

    // If I remove the dispatch_async block, this test passes fine. 
    // If I add it in again the test fails, probably because its not waiting
    it(@"should return the right count", ^{
        // entityCount is a block that performs a fetch request count
        NSInteger count = entityCount(moc, @"Task");
        [[theValue(count) should] equal:theValue(4)];
    })

    // This works fine, but obviously I don't want to wait a second
    it(@"should return the right count after waiting for a second", ^{
        sleep(1);
        NSInteger count = entityCount(moc, @"Task");
        [[theValue(count) should] equal:theValue(4)];
    });

};

If I remove the dispatch_async line, then I can get my test to run quickly. The only way I can get my test suite to run when using dispatch_async is to sleep(1) after calling the completion block. Using sleep() makes me think that I'm not approaching it in the right way. I have tried using shouldEventually but this doesn't seem to re-fetch my count value.

Was it helpful?

Solution

Have you tried these asynchronous block macros?

#define TestNeedsToWaitForBlock() __block BOOL blockFinished = NO
#define BlockFinished() blockFinished = YES
#define WaitForBlock() while (CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0, true) && !blockFinished)

OTHER TIPS

I have tried several approaches to solving this, none feel right.


1) Move the dispatch_async to its own class

+ (void)dispatchOnMainQueue:(Block)block
{
    if ([NSThread currentThread] == [NSThread mainThread]) {
        block();
    } else {
        dispatch_sync(dispatch_get_main_queue(), block);
    }
}

+ (void)dispatchOnBackgroundQueue:(Block)block
{
    dispatch_queue_t background = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
    dispatch_async(background, block);
}

Then during test execution, swizzle the background dispatch to occur on the main queue. This worked, but was unpredictable. It also felt so wrong!


2) Move the setup code to Kiwi's beforeAll block, then sleep the main thread. This works as the Kiwi tests are run on the main thread, so we're effectively saying "let the background operations happen before carrying on with the tests". I think this is what I'm going to use. Yes it makes my unit tests run slower, but they pass when they should do, and fail when they should

describe(@"MyAction", ^{

    __block void (^completionBlock)(NSArray *array, NSError *error);

    beforeAll(^{
        // Stub the http client
        id mockClient = [AFHTTPClient mock];
        [WRNAPIClient stub:@selector(sharedClient) andReturn:mockClient];

        // capture the block argument
        KWCaptureSpy *spy = [mockClient captureArgument:@selector(fetchAllThingsWithCompletion:) atIndex:0];
        [WRNTaskImporter importAllTasksFromAPI];
        completionBlock = spy.argument;

        // run the completion block 
        completionBlock(@[@"blah"], nil);

        // Wait for background import to complete
        [NSThread sleepForTimeInterval:0.1];

    })

    // This works 
    it(@"should return the right count", ^{
        // entityCount is a block that performs a fetch request count
        NSInteger count = entityCount(moc, @"Task");
        [[theValue(count) should] equal:theValue(4)];
    })
};

The caveat of this approach is that it only works when you aren't changing any data before a test. Say for example I insert 4 entities, and want to check each entity was inserted as expected. This option would work here. If I needed to re-run the import method and check that the count hadn't increased, I would need to add another [NSThread sleepForTimeInterval:0.1] after calling the insertion code.


For normal block based Kiwi tests you should probably use either the expectFutureValue shouldEventually method, or KWCaptureSpy to test your code, but this may not help when calling nested blocks.

If anyone has a more appropriate method for testing cases like these I'm happy to hear it!

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top