Using OCMock for iOS test
Using mock libraries ease your unit testing in isolation. But as discussed in
my previous post, we may end-up with a test oriented (over) layered design. Let's see how to test in isolation using mock but still leave a minimal footprint on your design.
Do your tests:
without checking everything
When a mock object receives a message that hasn't been either stubbed or expected, it throws an exception immediately and your test fails. This is called a strict mock behavior and this is just a pain...
Checking behavior rather than state, you want to write test easy to understand. Using nice mock, any type helps.
NOTE: What is the difference between
expect and
stub?
You may want to check the original post from Martin Fowler on
Mock aren't Stubs.
TD;DR; You
expect things that
must happen, and
stub things that
might happen. In OCMock context, when you call
verify on your mock (generally at the end of your test), it checks to make sure all of the methods you expected were actually called. If any were not, your test will fail. Methods that were stubbed are not verified.
Here we want to check the OAuth2 HTTP protocol.
it(@"should issue a request for exchanging authz code for access token", ^{
__block BOOL wasSuccessCallbackCalled = NO;
void (^callbackSuccess)(id obj) = ^ void (id object) {wasSuccessCallbackCalled = YES;};
void (^callbackFailure)(NSError *error) = ^ void (NSError *error) {};
id mockAGHTTPClient = [OCMockObject mockForClass:[AGHttpClient class]]; // [1]
NSString* code = @"CODE";
AGRestOAuth2Module* myRestAuthzModule = [[AGRestOAuth2Module alloc] initWithConfig:config client:mockAGHTTPClient]; // [3]
NSMutableDictionary* paramDict = [@{@"code":code, @"client_id":config.clientId, @"redirect_uri": config.redirectURL, @"grant_type":@"authorization_code"} mutableCopy];
[[mockAGHTTPClient expect] POST:config.accessTokenEndpoint parameters:paramDict success:[OCMArg any] failure:[OCMArg any]]; // [2]
[myRestAuthzModule exchangeAuthorizationCodeForAccessToken:code success:callbackSuccess failure:callbackFailure];
[mockAGHTTPClient verify];
[mockAGHTTPClient stopMocking];
});
In [1], we create a mock with an expectation [2], the important part for the test is checking URL endpoint and parameters, for the other arguments I'll simply put any type: [OCMArg any].
without Dependency Injection
In the previous example, in [3] we see an example where we inject our mock within a real object. there is some cases where DI is not easy, could we still mock without injecting?
For example here I want to mock the following call [[UIApplication sharedApplication] openURL:url] within the method under test requestAuthorizationCodeSuccess:failure:, here is a way to
it(@"should issue a request for authz code when no previous access grant was requested before", ^{
__block BOOL wasSuccessCallbackCalled = NO;
void (^callbackSuccess)(id obj) = ^ void (id object) {wasSuccessCallbackCalled = YES;};
void (^callbackFailure)(NSError *error) = ^ void (NSError *error) {};
//given a mock UIApplication
id mockApplication = [OCMockObject mockForClass:[UIApplication class]];
[[[mockApplication stub] andReturn:mockApplication] sharedApplication];
[[mockApplication expect] openURL:[OCMArg any]];
AGRestOAuth2Module* myRestAuthzModule = [[AGRestOAuth2Module alloc] initWithConfig:config];
[myRestAuthzModule requestAuthorizationCodeSuccess:callbackSuccess failure:callbackFailure];
[mockApplication verify];
[mockApplication stopMocking];
});
without splitting my classes in several layers
Without debating "one class should do one thing only" suppose, you have a class with several methods, you want to test one method and mock the other ones.
Here
requestAccessSuccess:failure: method delegate its call to either
refreshAccessTokenSuccess:failure: or
exchangeAccessTokenSuccess:failure: depending if an access token and expiration date are present.
it(@"should issue a refresh request when access token has expired", ^{
void (^callbackSuccess)(id obj) = ^ void (id object) {};
void (^callbackFailure)(NSError *error) = ^ void (NSError *error) {};
restAuthzModule.session.accessTokens = @"ACCESS_TOKEN";
restAuthzModule.session.refreshTokens = @"REFRESH_TOKEN";
restAuthzModule.session.accessTokensExpirationDate = 0;
// Create a partial mock of restAuthzModule
id mock = [OCMockObject partialMockForObject:restAuthzModule];
[[mock expect] refreshAccessTokenSuccess:[OCMArg any] failure:[OCMArg any]];
[restAuthzModule requestAccessSuccess:callbackSuccess failure:callbackFailure];
[mock verify];
[mock stopMocking];
});
I guess you got the idea. Test, whatever you need to test, don't go to close to the implementation.
Some may call it TDD vs. BDD, but I simply go: "Use what works best for you".