Xcode 5: Test UITableView with XCTest framework

bots

In WWDC 2013, Apple introduced Xcode 5 and iOS SDK 7 with a built-in framework for testing: XCTest.framework. Unfortunately Apple documentation lacks details for this framework. In this post I am going to present a simple way to test a UITableView using XCTest framework.

First, we need an Xcode project with a simple UITable. Open Xcode and create a new single-view application project. Open the storyboard file and drag a UITableView onto your view controller.

Storyboard

In the identity Inspector set “testTableView” as the Storyboard ID. Open up the ViewController.m file and place the code to poulate the dummy UITableView (do not forget to connect the IBOutlet to the TableView in the storyboard):

#import "ViewController.h"

@interface ViewController ()

@property (weak, nonatomic) IBOutlet UITableView *tableView;

@end

@implementation ViewController

#pragma mark - View loading methods
- (void)viewDidLoad
{
    [super viewDidLoad];
    self.title = @"XCTest Tutorial";
	// Do any additional setup after loading the view, typically from a nib.
}

#pragma mark - Memory warning methods
- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

#pragma mark - UITableView delegate methods
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return 15;
}

-(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return 1;
}

-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    NSString *reuseId = [NSString stringWithFormat:@"%ld/%ld",(long)indexPath.section,(long)indexPath.row];
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:reuseId];
    if(!cell)
    {
        cell =[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:reuseId];
        cell.selectionStyle = UITableViewCellSelectionStyleGray;

    }

    cell.textLabel.text = [NSString stringWithFormat:@"Row %ld", (long)indexPath.row+1];

    return cell;
}

@end

As you can see the code is pretty self-explanatory. We just create a UITableView property with 15 rows and print the row number in each row (note that in your ViewController.h file you have to declare that the controller conforms to UITableViewDatasource and UITableViewDelegate protocols).

As you can see, I declared the UITableView property in my .m file interface section (which is the correct way to do it, because there is no need in our case to expose the UITableView to our .h file so it can be accessed by other classes). But we need to access it from our XCtest class. The easy solution would be to just move the declaration to the .h file, right? Yes, but it is not the most sophisticated solution. What are we going to do, is create a class extension which exposes the UITableView property for us. To do this, navigate to File>New>New file and create a Objective-C class extension file.

ClassExtension

Name the file “Private” and select ViewController as the class. Open up the newly created header file and just redeclare the UITableView there as shown in the following snippet:

#import "ViewController.h"

@interface ViewController ()

@property (weak, nonatomic) IBOutlet UITableView *tableView;

@end

Connect the IBOutlet in the tableView in the storyboard again. Now we are ready to write some tests for our UITableVIew. Open up the Tests.m file which is automatically created by Xcode 5 with every new project. Import the private header file we just created and declare a ViewController property:

#import <XCTest/XCTest.h>
#import "ViewController_Private.h"

@interface XCTestTutorialTests : XCTestCase

@property (nonatomic, strong) ViewController *vc;

@end

The first thing we want to test is that the view loads and has a UITableView as subview. In the setup method we put the code we want to run before each test -in our case we just want to load the viewcontroller- and in the teardown method we put the code we want to run after each test invocation:

- (void)setUp
{
    [super setUp];
    UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
    self.vc = [storyboard instantiateViewControllerWithIdentifier:@"testTableView"];
    [self.vc performSelectorOnMainThread:@selector(loadView) withObject:nil waitUntilDone:YES];

    // Put setup code here. This method is called before the invocation of each test method in the class.
}

- (void)tearDown
{
    // Put teardown code here. This method is called after the invocation of each test method in the class.
    self.vc = nil;
    [super tearDown];
}

#pragma mark - View loading tests
-(void)testThatViewLoads
{
    XCTAssertNotNil(self.vc.view, @"View not initiated properly");
}

- (void)testParentViewHasTableViewSubview
{
    NSArray *subviews = self.vc.view.subviews;
    XCTAssertTrue([subviews containsObject:self.vc.tableView], @"View does not have a table subview");
}

-(void)testThatTableViewLoads
{
    XCTAssertNotNil(self.vc.tableView, @"TableView not initiated");
}

The testThatViewLoads method checks that the viewcontroller’s view is not nil. Pretty simple test to check that a view is initiated. The testParentViewHasTableViewSubview tests that our view has UITableView subview and the testThatTableViewLoads tests that our tableView is not nil. Next we have some tests for the UITableView property:

#pragma mark - UITableView tests
- (void)testThatViewConformsToUITableViewDataSource
{
    XCTAssertTrue([self.vc conformsToProtocol:@protocol(UITableViewDataSource) ], @"View does not conform to UITableView datasource protocol");
}

- (void)testThatTableViewHasDataSource
{
    XCTAssertNotNil(self.vc.tableView.dataSource, @"Table datasource cannot be nil");
}

- (void)testThatViewConformsToUITableViewDelegate
{
     XCTAssertTrue([self.vc conformsToProtocol:@protocol(UITableViewDelegate) ], @"View does not conform to UITableView delegate protocol");
}

- (void)testTableViewIsConnectedToDelegate
{
    XCTAssertNotNil(self.vc.tableView.delegate, @"Table delegate cannot be nil");
}

- (void)testTableViewNumberOfRowsInSection
{
    NSInteger expectedRows = 15;
    XCTAssertTrue([self.vc tableView:self.vc.tableView numberOfRowsInSection:0]==expectedRows, @"Table has %ld rows but it should have %ld", (long)[self.vc tableView:self.vc.tableView numberOfRowsInSection:0], (long)expectedRows);
}

- (void)testTableViewHeightForRowAtIndexPath
{
    CGFloat expectedHeight = 44.0;
    CGFloat actualHeight = self.vc.tableView.rowHeight;
    XCTAssertEqual(expectedHeight, actualHeight, @"Cell should have %f height, but they have %f", expectedHeight, actualHeight);
}

- (void)testTableViewCellCreateCellsWithReuseIdentifier
{
    NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
    UITableViewCell *cell = [self.vc tableView:self.vc.tableView cellForRowAtIndexPath:indexPath];
    NSString *expectedReuseIdentifier = [NSString stringWithFormat:@"%ld/%ld",(long)indexPath.section,(long)indexPath.row];
    XCTAssertTrue([cell.reuseIdentifier isEqualToString:expectedReuseIdentifier], @"Table does not create reusable cells");
}

The code is pretty easy to understand. Note that in every assertion we have an expression and a format. The format acts like [NSString stringWithFormat:] by default, so there is no need to use stringWithFormat there.

Unit Tests are an essential part of a modern development flow and is nice to see that Apple acknowledges it and provided a framework for that. Unfortunately XCTest does not offer a way to create mock objects. To do that you have to rely on a third party framework like OCMock.  You can download the demo project used in this tutorial from Github.

12 thoughts on “Xcode 5: Test UITableView with XCTest framework

  1. Great Tutorial, there’s not many articles about how to affront the unit testing and implement ttd in iOS with XCTests.

    Is really useful, in other hand do you read about TDD? And if is affirmative how you can confront the ttd development with the iOS creating cycle or how you work with unit testing (you first develop the view controller and later think in the tests or not???)

    Thanks again for your tuto.

  2. about the private class extension, do you reference this from the real view-controller I mean if later I change something of the private properties in the real .m class , because the test imports the private file, do you think that maybe is a good idea to import the private from the .m view controller file too? With this there’s only one Iboutlet connected …

    1. @Nscocoalab to reference a property from an other class the property have to be exposed to an interface file. We use the private class extension to expose the properties and reference them to our test classes. There is no need to expose them in the .h file of the class because they don’t need to be referenced from other classes in our app.

  3. Hi, thank you so much for detail explanation. I have one question.
    Can we check table view didSelectRowAtIndexPath action( i.e click action for table view)?
    For button, we follow below code. is there any code to determine UITableView click action.
    -(void)testNewUserButtonIBAction {
    NSArray *actions = [mLoginViewController.mNewUserBtn actionsForTarget:mLoginViewController forControlEvent:UIControlEventTouchUpInside];
    XCTAssertTrue([actions containsObject:@”newUserAction:”], @”newUserAction: Event Fired”);
    }

    Thanks in advance.

    1. @SureshD: You can get the touch point for the cell and from there you can get the indexPath. Knowing the indexpath will make it easy to determine the action which cell is selected and if it corresponds to the correct action.

      CGPoint center= sender.center;
      CGPoint rootViewPoint = [sender.superview convertPoint:center toView:self.tableView];
      NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint:rootViewPoint];

      1. @Nikon, how can I get the sender instance in test case method?
        When we run test cases for an app, we don’t get any user interaction to the application. In that case, how can a test case knows about sender event?
        When the app is in foreground( building mode ), your code will work give selected index.

  4. Hi Nikon,

    I have one more question for you. below test case method is failing, why?
    -(void)testPrevButtonIBAction {
    LandingViewController *mLandingViewController = [[LandingViewController alloc] initWithNibName:@”LandingViewController” bundle:nil];
    NSArray *actions = [mLandingViewController.mPrevBtn actionsForTarget:mLandingViewController forControlEvent:UIControlEventTouchUpInside];
    XCTAssertTrue([actions containsObject:@”prevAction:”], @”prevAction: Event Fired”);
    }
    When I debug the object, nib controls are not initialized. Why controls are not initialized in this case and how can I test a view-controller button action(above kind of scenarios).
    Thanks in advance.

      1. @Nikon, Thank you. The provide link might give me solution. I didn’t try it as I don’t have time to work on it.
        But, on last day, I have worked a lot to test a View Controller and its properties. Please find the below code snippet and advice me if there is any wrong.
        -(void)testNextButtonIBAction {
        AppDelegate* delegate = ((AppDelegate*) [[UIApplication sharedApplication] delegate]);
        LogMealsViewController *mLogMealsViewController = [[LogMealsViewController alloc] initWithNibName:@”LogMealsViewController” bundle:nil];
        UINavigationController *mNavigationController = [[UINavigationController alloc] initWithRootViewController:mLogMealsViewController];
        delegate.window.rootViewController = mNavigationController;
        [delegate.window makeKeyAndVisible];

        NSArray *actions = [mLogMealsViewController.mNext2Btn actionsForTarget:mLogMealsViewController forControlEvent:UIControlEventTouchUpInside];
        XCTAssertTrue([actions containsObject:@”nextAction:”], @”nextAction: Event Fired”);
        }

        The above method( approach ) is working fine for all the View-controllers. Here what I am doing is, In every test case method, I am creating corresponding view-controller object and assigning it to window’s rootViewController, then I am writing test scripts for my buttons. is this the right way? Please give your valuable comments on this approach.

  5. I think the reuse identifier implementation is wrong as it should be the same for each cell. In the current example instead is generated dynamically with the values of the current row and section

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.