Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
354 views
in Technique[技术] by (71.8m points)

objective c - How do you test an asynchronous method?

I have an object that fetches XML or JSON over a network. Once this fetching is complete it calls a selector, passing in the returned data. So, for example I'd have something like:

-(void)testResponseWas200
{
    [MyObject get:@"foo.xml" withTarget:self selector:@selector(dataFinishedLoading:)];  
}

I tried the route of implementing dataFinishedLoading in the Test class and attempting to test inside that method, but the test suite is just locking up. This seems like it's a case for mocking, but I'm wondering if others have encountered this and how they handled it.

FYI: I'm using gh-unit for testing and any method prefixed with test* is executed automatically.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

Three ways that come to mind are: NSRunLoop, semaphores, and groups.

NSRunLoop

__block bool finished = false;

// For testing purposes we create this asynchronous task 
// that starts after 3 seconds and takes 1 second to execute.
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0UL);
dispatch_time_t threeSeconds = dispatch_time(DISPATCH_TIME_NOW, 3LL * NSEC_PER_SEC);
dispatch_after(threeSeconds, queue, ^{ 
    sleep(1); // replace this with your task
    finished = true; 
});

// loop until the flag is set from inside the task
while (!finished) {
    // spend 1 second processing events on each loop
    NSDate *oneSecond = [NSDate dateWithTimeIntervalSinceNow:1];
    [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:oneSecond];
}

A NSRunLoop is a loop that processes events like network ports, keyboard, or any other input source you plug in, and returns after processing those events, or after a time limit. When there are no events to process, the run loop puts the thread to sleep. All Cocoa and Core Foundation applications have a run loop underneath. You can read more about run loops in Apple's Threading Programming Guide: Run Loops, or in Mike Ash Friday Q&A 2010-01-01: NSRunLoop Internals.

In this test, I'm just using the NSRunLoop to sleep the thread for a second. Without it, the constant looping in the while would consume 100% of a CPU core.

If the block and the boolean flag are created in the same lexical scope (eg: both inside a method), then the flag needs the __block storage qualifier to be mutable. Had the flag been a global variable, it wouldn't need it.

If the test crashes before setting the flag, the thread is stuck waiting forever. Add a time limit to avoid that:

NSDate *timeout = [NSDate dateWithTimeIntervalSinceNow:2];
while (!finished && [timeout timeIntervalSinceNow]>0) {
    [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode 
                             beforeDate:[NSDate dateWithTimeIntervalSinceNow:1]];
}
if (!finished) NSLog(@"test failed with timeout");

If you are using this code for unit testing, an alternative way to insert a timeout is to dispatch a block with an assert:

// taken from https://github.com/JaviSoto/JSBarrierOperationQueue/blob/master/JSBarrierOperationQueueTests/JSBarrierOperationQueueTests.m#L118
dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, 2LL * NSEC_PER_SEC);
dispatch_after(timeout, dispatch_get_main_queue(), ^(void){
    STAssertTrue(done, @"Should have finished by now");
});

Semaphore

Similar idea but sleeping until a semaphore changes, or until a time limit:

dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

// signal the semaphore after 3 seconds using a global queue
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0UL);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 3LL*NSEC_PER_SEC), queue, ^{ 
    sleep(1);
    dispatch_semaphore_signal(semaphore);
});

// wait with a time limit of 5 seconds
dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, 5LL*NSEC_PER_SEC);
if (dispatch_semaphore_wait(semaphore, timeout)==0) {
    NSLog(@"success, semaphore signaled in time");
} else {
    NSLog(@"failure, semaphore didn't signal in time");
}

dispatch_release(semaphore);

If instead we waited forever with dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); we would be stuck until getting a signal from the task, which keeps running on the background queue.

Group

Now imagine you have to wait for several blocks. You can use an int as flag, or create a semaphore that starts with a higher number, or you can group the blocks and wait until the group is finished. In this example I do the later with just one block:

dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0UL);

// dispatch work to the given group and queue
dispatch_group_async(group,queue,^{
    sleep(1); // replace this with your task
});

// wait two seconds for the group to finish
dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, 2LL*NSEC_PER_SEC);
if (dispatch_group_wait(group, timeout)==0) {
    NSLog(@"success, dispatch group completed in time");
} else {
    NSLog(@"failure, dispatch group did not complete in time");
}

dispatch_release(group);

If for some reason (to clean up resources?) you want to run a block after the group is finished, use dispatch_group_notify(group,queue, ^{/*...*/});


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...