objective-casynchronousxctestxctestexpectation

Manage multiple asynchronous operation on XCTests


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?


Solution

  • 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...]