Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unit Testing with NSURLSession for OCMock

I have a networking class called: ITunesAlbumDataDownloader

@implementation AlbumDataDownloader

- (void)downloadDataWithURLString:(NSString *)urlString
                completionHandler:(void (^)(NSArray *, NSError *))completionHandler
{        
    NSURLSession *session = [NSURLSession sharedSession];

    NSURLSessionDataTask *dataTask = [session dataTaskWithURL:[NSURL URLWithString:urlString]
                                            completionHandler:^(NSData *data, NSURLResponse *response, NSError *error)
    {        
        NSArray *albumsArray = [self parseJSONData:data];

        completionHandler(albumsArray, error);
    }];

    [dataTask resume];
}


    - (NSArray *)parseJSONData:(NSData *)data {

        NSMutableArray *albums = [[NSMutableArray alloc] init];

...
...

        // Return the array
        return [NSArray arrayWithArray:albums];
    }

    @end

and i need to create a Unit Test for this which does the following:

  • The NSURLSession dataTaskWithRequest:completionHandler: response is mocked to contain the fake JSON data i have:

// Expected JSON response

NSData *jsonResponse = [self sampleJSONData];
  • The returned array from the public method downloadDataWithURLString:completionHandler: response should contain all the albums and nil error.

Other points to bare in mind is that i need to mock NSURLSession with the fake JSON data "jsonResponse" to the downloadDataWithURLString:completionHandler: method WITHOUT invoking an actual network request.

I have tried various different things but i just can not work it out, i think its the combination of faking the request and the blocks which is really confusing me.

Here is two examples of my test method that i tried (i actually tried a lot of other ways also but this is what i have remaining right now):

- (void)testValidJSONResponseGivesAlbumsAndNilError {

    // Given a valid JSON response containing albums and an AlbumDataDownloaderTests instance

    // Expected JSON response
    NSData *jsonResponse = [self sampleJSONDataWithAlbums];

    id myMock = [OCMockObject mockForClass:[NSURLSession class]];

    [[myMock expect] dataTaskWithRequest:OCMOCK_ANY
                       completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error)
    {

    }];

    [myMock verify];
}

and

- (void)testValidJSONResponseGivesAlbumsAndNilError {

                // Given a valid JSON response containing albums and an AlbumDataDownloaderTests instance

                // Expected JSON response
                NSData *jsonResponse = [self sampleJSONDataWithAlbums];

            id myMock = [OCMockObject mockForClass:[AlbumDataDownloader class]];

            [[[myMock stub] andReturn:jsonResponse] downloadDataWithURLString:OCMOCK_ANY
                                                            completionHandler:^(NSArray *response, NSError *error)
            {

            }];

            [myMock verify];
            }
        }

I have a feeling that in both instances I'm probably way off the mark :(

I would really appreciate some help with this.

Thanks.

UPDATE 1:

Here is what i have now come up with but need to know if I'm on the right track or still making a mistake?

id mockSession = [OCMockObject mockForClass:[NSURLSession class]];
id mockDataTask = [OCMockObject mockForClass:[NSURLSessionDataTask class]];


[[mockSession stub] dataTaskWithRequest:OCMOCK_ANY
                        completionHandler:^(NSData  _Nullable data, NSURLResponse  Nullable response, NSError * Nullable error)
{
    NSLog(@"Response: %@", response);
}];


[[mockDataTask stub] andDo:^(NSInvocation *invocation)
{
    NSLog(@"invocation: %@", invocation);
}];
like image 681
GameDev Avatar asked Nov 26 '15 04:11

GameDev


1 Answers

The trick with blocks is you need the test to call the block, with whatever arguments the test wants.

In OCMock, this can be done like this:

OCMStub([mock someMethodWithBlock:([OCMArg invokeBlockWithArgs:@"First arg", nil])]);

This is convenient. But…


The downside is that the block will be invoked immediately when someMethodWithBlock: is called. This often doesn't reflect the timing of production code.

If you want to defer calling the block until after the invoking method completes, then capture it. In OCMock, this can be done like this:

__block void (^capturedBlock)(id arg1);
OCMStub([mock someMethodWithBlock:[OCMArg checkWithBlock:^BOOL(id obj) {
    capturedBlock = obj;
    return YES;
}]]);

// ...Invoke the method that calls someMethodWithBlock:, then...
capturedBlock(@"First arg"); // Call the block with whatever you need

I prefer to use OCHamcrest's HCArgumentCaptor. OCMock supports OCHamcrest matchers, so I believe this should work:

HCArgumentCaptor *argument = [[HCArgumentCaptor alloc] init];
OCMStub([mock someMethodWithBlock:argument]);

// ...Invoke the method that calls someMethodWithBlock:, then...
void (^capturedBlock)(id arg1) = argument.value; // Cast generic captured argument to specific type
capturedBlock(@"First arg"); // Call the block with whatever you need
like image 155
Jon Reid Avatar answered Oct 05 '22 23:10

Jon Reid