Episode #83

TDD View Controllers Part 2

18 minutes
Published on September 5, 2013

This video is only available to subscribers. Get access to this video and 572 others.

We continue our TDD exercise, building a view controller one test at a time using Specta and OCMock. This time we introduce the alert view behavior for invalid logins, performing a segue for valid logins, and capturing arguments passed to mocks in order to affect the system, all in tests.

Episode Links

Extracting common parts into a beforeEach

 describe(@"logging in", ^{
        __block id _mockLoginService;

        beforeEach(^{
            _mockLoginService = [OCMockObject mockForClass:[LoginService class]];
            _vc.loginService = _mockLoginService;
        });

        afterEach(^{
            [_mockLoginService verify];
        });

        it(@"should verify username & password with the login service", ^{
            [[_mockLoginService expect] verifyUsername:@"user1"
                                             password:@"password1"
                                           completion:[OCMArg any]];

            _vc.usernameTextField.text = @"user1";
            _vc.passwordTextField.text = @"password1";

            [_vc loginTapped:nil];
        });
  })

Introducing a context for invalid login

We don't want to actually depend on any existing behavior for valid or invalid logins, we simply want to invoke the proper response by passing in NO to the provided block. To do this, we need to capture whatever block was passed to our mock object.

context(@"with invalid credentials", ^{
            __block id _alertProvider;

            beforeEach(^{
                _alertProvider = [OCMockObject mockForClass:[AlertViewProvider class]];
                _vc.alertProvider = _alertProvider;

                [[_mockLoginService stub] verifyUsername:[OCMArg any]
                                                password:[OCMArg any]
                                              completion:[OCMArg checkWithBlock:^BOOL(LoginServiceCompletionBlock block) {
                    block(NO);
                    return YES;
                }]];
            });
         ...
   })

The alert provider is an injectable dependency that gives us control over how alerts are created. This lets us provide mocks when necessary.

@interface AlertViewProvider : NSObject
- (UIAlertView *)alertViewWithTitle:(NSString *)title message:(NSString *)message;
@end

@implementation AlertViewProvider
- (UIAlertView *)alertViewWithTitle:(NSString *)title message:(NSString *)message {
    return [[UIAlertView alloc] initWithTitle:title
                                      message:message
                                     delegate:nil
                            cancelButtonTitle:@"OK"
                            otherButtonTitles:nil];
}
@end

Mocking an alert view to assert that it was shown

 it(@"should show an alert", ^{
   id mockAlert = [OCMockObject mockForClass:[UIAlertView class]];
   [[[_alertProvider expect] andReturn:mockAlert] alertViewWithTitle:[OCMArg any]
                                                             message:[OCMArg any]];
   [[mockAlert expect] show];

   [_vc loginTapped:nil];

   [_alertProvider verify];
   [mockAlert verify];
 });

Testing the valid login case

The valid login case just needs to set up the environment to invoke the completion block with a YES parameter, and then assert that the WelcomeViewController was pushed onto the navigation stack.


        context(@"valid credentials", ^{
            beforeEach(^{
                [[_mockLoginService stub] verifyUsername:[OCMArg any]
                                                password:[OCMArg any]
                                              completion:[OCMArg checkWithBlock:^BOOL(LoginServiceCompletionBlock block) {
                    block(YES);
                    return YES;
                }]];
            });

            it(@"should push the welcome view controller", ^{
                [_vc loginTapped:nil];

                expect(_vc.navigationController.visibleViewController).to.beInstanceOf([WelcomeViewController class]);
            });
        });

Note: If you get an error about "expecting WelcomeViewController but got WelcomeViewController then uncheck the class from the test target. Read this article on View Controller Testing for a good explanation of why this can happen.