Thursday, May 15, 2014

Testing without design footprint

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".

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.