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:
// Expected JSON response
NSData *jsonResponse = [self sampleJSONData];
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);
}];
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…
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With