I'm facing with a problem with asynchronous operations in a XCTestCase object.
I started with a chain of expectation-waitForExpectation
, sometimes passing the expectation instance as a method param to make it fulfilled by the asynchronous operation completion block.
Now I changed the method, because I can't see bad written code as it was before and I tried in this way:
- (void)testThatItGoesToTheRightSport
{
if (! appDelegateInstance.firstTimeLoaded)
{
[self __waitForLoadingAppAndDo:^(BOOL launched)
{
if (launched)
{
[self __goToLiveWithHandler:^(BOOL success) {
if (success)
{
[self __goToLiveSport:[NSNumber numberWithUnsignedInteger:kDefaultSportCode]
handler:^(BOOL success) {
if (! success)
{
XCTFail(@"Test failed");
}
}];
}
}];
}
}];
}
else
{
[self __goToLiveWithHandler:^(BOOL success) {
if (success)
{
[self __goToLiveSport:[NSNumber numberWithUnsignedInteger:kDefaultSportCode]
handler:^(BOOL success) {
if (! success)
{
XCTFail(@"Test failed");
}
}];
}
}];
}
}
With __waitForLoadingAppAndDo:
method implemented as
- (void)__waitForLoadingAppAndDo:(void (^)(BOOL launched))afterBlock
{
if (! afterBlock)
XCTFail(@"No afterBlock");
XCTestExpectation *dataEx = [self expectationForNotification:NOTIFICATION_HOME_LOADED
object:nil
handler:^BOOL(NSNotification *notification) {
[dataEx fulfill];
return YES;
}];
[self waitForExpectationsWithTimeout:SOCKOPT_TIMEOUT handler:^(NSError *error)
{
if (error)
{
XCTFail(@"No data received. %@", [error localizedDescription]);
}
else
{
dispatch_async(dispatch_get_main_queue(), ^{
afterBlock(YES);
});
}
}];
}
And the other __
methods are similar to it. Obviously, now, the testThat
method is not waiting for the methods completion handler. How can I improve it and how can I make testThat
methods wait for completion? Is XCTestExpectation
the way? (tell me not eheh)
ADDING: so, is that the unique way?
- (void)testThatItGoesToTheRightSport
{
if (! appDelegateInstance.firstTimeLoaded)
{
XCTestExpectation *waitingLoading = [self expectationWithDescription:@"loadingApp"];
[self waitForExpectationsWithTimeout:SOCKOPT_TIMEOUT handler:^(NSError *error) {
if (error)
{
XCTFail(@"After block was not called.");
}
}];
[self __waitForLoadingAppAndDo:^(BOOL launched)
{
if (launched)
{
[self __goToLiveWithHandler:^(BOOL success) {
if (success)
{
[waitingLoading fulfill];
[...]
}
}];
}
}];
ADDING-2:
I tried with
__block NSError *internalError = nil;
__block XCTestExpectation *finishedTest = [self expectationWithDescription:@"finishedTest"];
dispatch_group_t asyncGroup = dispatch_group_create();
if (! appDelegateInstance.firstTimeLoaded)
{
dispatch_group_enter(asyncGroup);
[self __waitForLoadingAppAndDo:^(BOOL launched) {
if (! launched)
{
internalError = [TestUtilities notLoadedAppError];
}
dispatch_group_leave(asyncGroup);
}];
dispatch_group_enter(asyncGroup);
[self __goToLiveWithHandler:^(BOOL success) {
if (! success)
{
internalError = [NSError errorWithDomain:@"goLive"
code:-1
userInfo:@{NSLocalizedDescriptionKey : @"Errore apertura Live"}];
}
dispatch_group_leave(asyncGroup);
}];
dispatch_group_notify(asyncGroup, dispatch_get_main_queue(), ^{
[finishedTest fulfill];
});
but the groups are called asyncrhonously, without waiting for completion block. For example: I'm expecting that OP1 starts first and, in the OP1 completion block OP2 will start.
ADDING-3:
I used a different approach with dispatch_semaphore
. I like it, but there is a thing that I would replace. If I will wait for the signal, I'm blocking the main thread, so I have to dispatch_async
the wait
command as below:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
dispatch_semaphore_wait(loadedApp, DISPATCH_TIME_FOREVER);
dispatch_async(dispatch_get_main_queue(), ^{
[self __goToLiveWithHandler:^(BOOL success) {
if (success)
{
[...]
}
}];
});
});
Is there a way to avoid this?
Use
dispatch_group
To create a chain of events.
At the start of your test:
Dispatch_group_t group = dispatch_group_create();
Before each async portion call:
Dispatch_group_enter(group);
And when each async portion has finished use:
Dispatch_group_leave(group);
(The number of "enter" must equal the number of "leave")
At the end of the async code "wait" :
// called when "group" |enter|=|leave|
Dispatch_group_apply(group,main_queue,^{
// check your conditions....
If (success) [expectation fulfill];
Else // failure
});
Expectation wait....// add the expectation wait handler as before
There are different variations of dispatch_group
so you may need to tweak according to your use case.
Hope it helps
*******EDIT*******
Per your comment, you may want to nest the groups differently. For instance:
// create the expectation
Expectation = ....
dispatch_group_t myGroup = dispatch_group_create();
dispatch_group_enter(myGroup);
// do first portion
dispatch_group_leave(myGroup);
// do the second portion after the first
dispatch_group_apply(myGroup,dispatch_queue..., ^{
//second code portion
// chain as many as needed using this technique
// eventually you will need to fulfill your "expectation
// ...
if (success) {
[expectation fullfil];
} else {
XCTAssert(fail,@"The test failed because ....");
}
});
[self expectation wait...]