Search code examples
iosobjective-cunit-testingnsurlsessionnsurlrequest

How to write a unit test for a method with a completion handler that returns data?


I'm stuck trying to figure out how to write a unit test for such a method using OCMock. Can someone help me with it?

- (void)executeRequest:(NSURLRequest *)request withCompletionHandler:(void (^)(id responseData, NSError *error))completionHandler
{
    NSURLSession *session = [NSURLSession sharedSession];
    NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
    if (error)
        {
            NSLog(@"Network error occurred: %@", [error localizedDescription]);
            dispatch_async(dispatch_get_main_queue(), ^(void) { completionHandler(nil, error);
            });
            return;
        }
    if ([response isKindOfClass:NSHTTPURLResponse.class])
    {
        NSInteger statusCode = [(NSHTTPURLResponse *)response statusCode];
        if (statusCode != 200)
        {
            NSLog(@"Network error occurred. Status code: %ld", (long)statusCode);
            return;
        }
    }
        completionHandler(data, nil);
    }];
    [dataTask resume];
}

Solution

  • You should use OCMArg with checkWithBlock or invokeBlockWithArgs to test completions handlers. Here an example:

    1. Create a XCTestCase subclass by click command+N:

    New_file

    1. Add properties for testable instance and necessary mocks:

      @interface SomeClassTests : XCTestCase
      
      @property (nonatomic, strong) SomeClass *testableInstance;
      @property (nonatomic, strong) NSURLSession *mockSession;
      @property (nonatomic, strong) NSURLRequest *mockRequest;
      @property (nonatomic, strong) NSHTTPURLResponse *mockResponse;
      
      @end 
      
    2. Setup properties:

      - (void)setUp
      {
          [super setUp];
          self.testableInstance = [SomeClass new];
          self.mockSession = OCMClassMock([NSURLSession class]);
          self.mockRequest = OCMClassMock([NSURLRequest class]);
          self.mockResponse = OCMClassMock([NSHTTPURLResponse class]);
          OCMStub(ClassMethod([(id)self.mockSession sharedSession])).andReturn(self.mockSession);
      }
      
    3. Don't forget to clean up at tear down:

      - (void)tearDown
      {
          [(id)self.mockSession stopMocking];
          self.mockResponse = nil;
          self.mockRequest = nil;
          self.mockSession = nil;
          self.testableInstance = nil;
          [super tearDown];
      }
      
    4. Let's test the case when an error is occurs:

      - (void)testWhenErrorOccuersThenCompletionWithSameError
      {
          // arrange
          NSError *givenError = [[NSError alloc] initWithDomain:@"Domain" code:0 userInfo:nil];
          OCMStub([self.mockSession dataTaskWithRequest:[OCMArg any] completionHandler:([OCMArg invokeBlockWithArgs:@"", self.mockResponse, givenError, nil])]);
      
          void (^givenCompletion)(id  _Nonnull, NSError * _Nonnull) = ^void(id  _Nonnull responseData, NSError * _Nonnull resultError) {
              // assert
              XCTAssertNil(responseData);
              XCTAssertEqual(resultError, givenError);
          };
      
          // act
          [self.testableInstance executeRequest:self.mockRequest withCompletionHandler:givenCompletion];
      }
      

    So we will sure that if some error occurs then the completion handler will invokes with same error at the argument.

    1. Let's test when we get some bad status code:

      - (void)testWhenBadStatusCodeThenReturnWithoutCompletion
      {
          // arrange
          OCMStub([self.mockResponse statusCode]).andReturn(403);
          OCMStub([self.mockSession dataTaskWithRequest:[OCMArg any] completionHandler:([OCMArg checkWithBlock:^BOOL(id param) {
              void (^passedCompletion)(NSData *data, NSURLResponse *response, NSError *error) = param;
              passedCompletion(nil, self.mockResponse, nil);
              return YES;
          }])]);
      
          void (^givenCompletion)(id  _Nonnull, NSError * _Nonnull) = ^void(id  _Nonnull responseData, NSError * _Nonnull resultError) {
              // assert
              XCTFail("Shouldn't be reached");
          };
      
          // act
          [self.testableInstance executeRequest:self.mockRequest withCompletionHandler:givenCompletion];
      }
      
    2. And finally lets test when we actually get data:

      - (void)testWhenSuccesThenCompletionWithSameData
      {
          // arrange
          NSData *givenData = [NSData data];
          OCMStub([self.mockResponse statusCode]).andReturn(200);
          OCMStub([self.mockSession dataTaskWithRequest:[OCMArg any] completionHandler:([OCMArg checkWithBlock:^BOOL(id param) {
              void (^passedCompletion)(NSData *data, NSURLResponse *response, NSError *error) = param;
              passedCompletion(givenData, self.mockResponse, nil);
              return YES;
          }])]);
      
          void (^givenCompletion)(id  _Nonnull, NSError * _Nonnull) = ^void(id  _Nonnull responseData, NSError * _Nonnull resultError) {
              // assert
              XCTAssertEqual(responseData, givenData);
          };
      
          // act
          [self.testableInstance executeRequest:self.mockRequest withCompletionHandler:givenCompletion];
      }
      

    If you will switch on a coverage then you will see that such test fully cover testable code:

    Coverage