Reputation: 5563
I am using XCTestExpectation
in a lot of tests and sometimes (very randomly) some expectations are not fulfilled (although I am sure they should be).
While investigating this problem I have noticed that some expectations are fulfilled in a main thread and some are fulfilled in a background thread. And so far these problems are with the ones fulfilled in a background thread.
Is it safe to fulfill expectations from a background thread? I could not find any explicit information about that.
Below is an example of how I use XCTestExpectation
:
__block XCTestExpectation *expectation = [self expectationWithDescription:@"test"];
[self doSomethingAsyncInBackgroundWithSuccess:^{
[expectation fullfill];
}];
[self waitForExpectationsWithTimeout:10.0 handler:^(NSError *error) {
expectation = nil;
if (error) {
NSLog(@"Timeout Error: %@", error);
}
}];
Upvotes: 3
Views: 2521
Reputation: 8029
It's not documented anywhere that XCTestExpectation is thread-safe. due to there being no official documentation on the matter you can only guess by creating test examples:
- (void)testExpectationMainThread;
{
__block XCTestExpectation *expectation = [self expectationWithDescription:@"test"];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[expectation fulfill];
});
[self waitForExpectationsWithTimeout:2 handler:^(NSError * _Nullable error) {
NSLog(@"%@", error);
}];
}
- (void)testExpectationStartMainThreadFulfilBackgroundThread;
{
__block XCTestExpectation *expectation = [self expectationWithDescription:@"test"];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, kNilOptions), ^{
[expectation fulfill];
});
[self waitForExpectationsWithTimeout:2 handler:^(NSError * _Nullable error) {
NSLog(@"%@", error);
}];
}
- (void)testExpectationBackgroundThread;
{
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, kNilOptions);
__block XCTestExpectation *expectation;
dispatch_sync(queue, ^{
expectation = [self expectationWithDescription:@"test"];
});
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), queue, ^{
[expectation fulfill];
});
[self waitForExpectationsWithTimeout:2 handler:^(NSError * _Nullable error) {
NSLog(@"%@", error);
}];
}
Here it does not crash or cause a problem however due to the lack of official documentation it is probably safer to stick to the same queue to fulfil.
you should really be stubbing the method doSomethingAsyncInBackgroundWithSuccess
and provide the app with local "dummy" data.
Your unit tests should not rely on network as it is something which is variable.
You should be executing the completion block of doSomethingAsyncInBackgroundWithSuccess
on the main thread (or at least provide a way to call back consistently on the same thread), you can easily do this with GCD.
- (void)doSomethingAsyncInBackgroundWithSuccess:(void (^)(void))completion;
{
dispatch_async(dispatch_get_main_queue(), ^{
completion();
});
}
or use NSOperationQueue mainQueue
- (void)doSomethingAsyncInBackgroundWithSuccess:(void (^)(void))completion;
{
[NSOperationQueue.mainQueue addOperationWithBlock:^{
completion();
}];
}
Upvotes: 4