iOS Unit Testing - Testing View Controller (Part 2)

This is the part 2 in the series of articles on 'Unit Testing on iOS'. Below is the list of all articles and respective links to them.

  1. iOS Unit Tests - Testing models creation (Part 1)
  2. iOS Unit Testing - Testing View Controller (Part 2)
  3. iOS Unit Testing - Switching method implementations with OCMock (Part 3)
  4. iOS Testing - Testing asynchronous code (Part 4)
  5. iOS Testing - Input fields validation testing (Part 5)

In this second part of Unit Testing on iOS we will see how you can test View controllers.

You can find the full source code along with unit tests in GitHub repository

I have following view controller which is under testing.


@interface JKSearchBrideViewController ()

@property (weak, nonatomic) IBOutlet UISearchBar *searchBar;
@property (weak, nonatomic) IBOutlet UISegmentedControl *segmentationView;
@property (weak, nonatomic) IBOutlet UITableView *tableView;

@end

@implementation JKSearchBrideViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.viewModel = [[JKBridesSearchViewModel alloc] init];
    self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] bk_initWithTitle:@"Filter" style:UIBarButtonItemStylePlain handler:^(id sender) {
        JKUnitTestingFilterViewController* filterVC = [[JKUnitTestingFilterViewController alloc] init];
        UINavigationController* nav = [[UINavigationController alloc] initWithRootViewController:filterVC];
        [self presentViewController:nav animated:YES completion:NULL];
    }];
    [[[_searchBar rac_textSignal] ignore:nil] subscribeNext:^(id x) {
        _viewModel.searchString = x;
    }];
    
    [[_segmentationView rac_newSelectedSegmentIndexChannelWithNilValue:nil] subscribeNext:^(NSNumber* x) {
        _viewModel.searchByString  = [x integerValue] == 0 ? @"name" : @"cast";
    }];
    
    [[RACObserve(self.viewModel, searchedBrides) ignore:nil] subscribeNext:^(NSArray* brides) {
        [self.tableView reloadData];
    }];
    
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return _viewModel.searchedBrides.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell* cell = [self.tableView dequeueReusableCellWithIdentifier:@"cell" forIndexPath:indexPath];
    JKBride* bride = _viewModel.searchedBrides[indexPath.row];
    cell.textLabel.text = [NSString stringWithFormat:@"%@ %@", bride.name, bride.cast];
    return cell;
}

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    [_viewModel selectBrideAtIndex:indexPath.row];
}

@end

As you might have guessed I am using MVVM architecture to structure my code. Pretty cool huh? MVVM is proven programming paradigm to facilitate testing. I love it and believe me, it's even better than MVC.

Here are the unit tests I wrote to verify the functionality. For clarity I have added frequent comments.


SpecBegin(ViewModelTests)

__block NSArray* brides;
__block JKBridesSearchViewModel* viewModel;
__block JKSearchBrideViewController* vc;
__block UINavigationController* navController;

// Before even test is started, we will construct our object and view models.
beforeAll(^{
    NSDictionary* bridesDict = @{
        @"success": @1,
        @"brides": @[
                   @{
                       @"first_name": @"Berta",
                       @"last_name": @"Johnson",
                       @"city": @"Boston",
                       @"cast": @"Wanjari",
                       @"married": @1,
                       @"education": @"masters",
                       @"income": @60000
                   },
                   @{
                       @"first_name": @"Leena",
                       @"last_name": @"Crident",
                       @"city": @"Pune",
                       @"cast": @"CKP",
                       @"married": @0,
                       @"education": @"bachelors",
                       @"income": @30000
                   },
                   @{
                       @"first_name": @"Jacqueline",
                       @"last_name": @"Sharp",
                       @"city": @"Virar",
                       @"cast": @"Pachkalshi",
                       @"married": @0,
                       @"education": @"bachelors",
                       @"income": @10000
                   }
                   ]
        };
    
// This is an array which is collection of JKBride objects.
    brides = [bridesDict[@"brides"] bk_map:^id(id obj) {
        return [[JKBride alloc] initWithDictionary:obj];
    }];
    id classMock = OCMClassMock([JKBridesProvider class]);
    OCMStub(ClassMethod([classMock brides])).andReturn([RACSignal return:brides]);
    
    viewModel = [[JKBridesSearchViewModel alloc] init];
});

describe(@"Verifying is table View is displayed on the screen and additional tests for testing UITableView", ^{
    
    before(^{
        navController = [[UIStoryboard storyboardWithName:@"Main" bundle:nil] instantiateInitialViewController];
        vc = (JKSearchBrideViewController*)navController.topViewController;
// Set viewModel to the view controller.
        vc.viewModel = viewModel;
        [UIApplication sharedApplication].keyWindow.rootViewController = navController;
// We need to call loadView manually to load the view. This will help us test if view is loaded and visible on the screen.
        [vc loadView];
    });
    
    it(@"Verifying the valid views", ^{
// Here we will verify if navigation controller and view are correctly displayed on the viewport.        XCTAssertNotNil(navController.topViewController);
        XCTAssertNotNil(vc.view);
    });
    
// Since we have 3 records, tableView will have total 3 rows.
    it(@"Verifying the number of rows", ^{
        XCTAssert([vc tableView:[UITableView new] numberOfRowsInSection:0] == 3);
    });
    
// We will verify if correct text is shown on the tableView cell.
    it(@"Verifying the cell model", ^{
        UITableViewCell* firstCell = [vc tableView:[UITableView new] cellForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]];
        XCTAssert([firstCell.textLabel.text isEqualToString:@"Berta Johnson Wanjari"]);
        
        UITableViewCell* secondCell = [vc tableView:[UITableView new] cellForRowAtIndexPath:[NSIndexPath indexPathForRow:1 inSection:0]];
        XCTAssert([secondCell.textLabel.text isEqualToString:@"Leena Crident CKP"]);
        
    });
    
// Testing if our view controller adheres to UITableView protocols.
    it(@"Verifying if vc conforms to tableView protocols", ^{
        XCTAssertTrue([vc conformsToProtocol:@protocol(UITableViewDelegate)]);
        XCTAssertTrue([vc conformsToProtocol:@protocol(UITableViewDataSource)]);
    });
    
// Testing the cell identifier for UITableViewCell.
    it(@"Verifying the table view cell reuse identifier", ^{
        UITableViewCell* cell = [vc tableView:[UITableView new] cellForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]];
        XCTAssertTrue([cell.reuseIdentifier isEqualToString:@"cell"]);
    });
    
// We will manually select a cell in UITableView. Our viewModel will then appropriately update to set the current bride model to the one selected by the user.

    it(@"Verifying the cell selection makes appropriate model selected on search view model", ^{
        [vc tableView:[UITableView new] didSelectRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]];
        JKBride* selectedBride = viewModel.selectedBride;
        XCTAssert([selectedBride.name isEqualToString:@"Berta Johnson"]);
        XCTAssert([selectedBride.city isEqualToString:@"Boston"]);
        XCTAssert([selectedBride.cast isEqualToString:@"Wanjari"]);
        XCTAssert(selectedBride.married == true);
        XCTAssert([selectedBride.education isEqualToString:@"masters"]);
    });
});

SpecEnd

In the next post I will write about switching method implementation during testing using OCMock framework

Go to next article