I am writing a unit test for a Data Source object, with its customary delegate object. What this object does is fetching some data from a certain web service, and then call back to the delegate to notify success. Here is the code:
NSString *validProductId = @"34142977";
NSString *validSiteCode = @"someSiteCode";
[[dataSourceDelegateMock expect] dataSource:dataSource didFetchProductData:[OCMArg any] forProductWithId:validProductId];
[dataSource fetchProductWithId:validProductId andSiteCode:validSiteCode];
NSDate *runUntilDate = [NSDate dateWithTimeIntervalSinceNow:networkTimeOut];
while ([runUntilDate timeIntervalSinceNow] > 0) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:runUntilDate];
}
[dataSourceDelegateMock verify];
Now this is working fine and all, and as long as the network replies with some data within 10 seconds, the test will succeed as it should.
I am aware this is not how some would test networking code. Some would do it differently, some would call this an integration test, but that's not what I am interested in at this point.
Here is the problem: the code above will work fine, however it will run for 10 seconds every time, regardless of the fact that the network is usually much faster than that.
What I am trying to do now is to add another condition in my while loop, whose meaning is "if we haven't timed out yet AND if the network hasn't replied already". In this way, I could make the test execute much faster most of the times.
So I modified the test as follows:
NSString *validProductId = @"34142977";
NSString *validSiteCode = @"someSiteCode";
__block BOOL dataSourceFetchedData = NO;
[[dataSourceDelegateMock expect] dataSource:dataSource didFetchProductData:[OCMArg any] forProductWithId:validProductId];
[[[dataSourceDelegateMock stub] andDo:^(NSInvocation * invocation) {
dataSourceFetchedData = YES;
}] dataSource:dataSource didFetchProductData:[OCMArg any] forProductWithId:[OCMArg any]];
[dataSource fetchProductWithId:validProductId andSiteCode:validSiteCode];
NSDate *runUntilDate = [NSDate dateWithTimeIntervalSinceNow:networkTimeOut];
while ([runUntilDate timeIntervalSinceNow] > 0 && dataSourceFetchedData == NO) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:runUntilDate];
}
[dataSourceDelegateMock verify];
Obviously the idea here is that as soon as the delegate callback is sent to the mock delegate object (didFetchProductData...) the bool variable will be set to YES and the while loop will terminate, thus shortening the duration of the test itself.
Finally to the problem: no matter what I do, I can't match the signature that will be called at runtime for the delegate callback, so whatever I put inside the block will never be executed. The test will still work, but it won't be any faster.
After much debugging, I pinpointed the problem on the validProductId variable. For some reason I can't understand, the returned value will never match the expectations I set up when stubbing the method. How do I know ? Because if I set the expectation to nil, and force the data source to return nil, everything will work fine.
I have tried everything I could think of, so any help will be MUCH appreciated. This is the data source method that calls back:
(void) fetchProductWithId:(NSString *)productId andSiteCode:(NSString *)siteCode {
NSString *urlString = [NSString stringWithFormat:itemApiString,siteCode,productId];
NSURL *url = [NSURL URLWithString:urlString relativeToURL:self.baseUrl];
[self.requestManager GET:url.absoluteString parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
[self.delegate dataSource:self didFetchProductData:responseObject forProductWithId:productId];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
[self.delegate dataSource:self didFailFetchProductDataForProductId:productId withError:error];
}];
}
As you see the data source is returning exactly the same productId it is passed, so I really can't understand why on earth that wouldn't match the expected stubbed method.
Thanks very much.
You can combine expect
and stub
but the interaction is not what most people expect. The expect handles the first call to the method and, once it's satisfied, becomes inactive, allowing the stub to handle further invocations.
That said, it is possible to add actions to expect, and I believe that is what you want; like this:
[[[dataSourceDelegateMock expect] andDo:^(NSInvocation * invocation) {
dataSourceFetchedData = YES;
}] dataSource:dataSource didFetchProductData:[OCMArg any]]