Unit testing UITableViewCell

It might not seem obvious but unit testing data that exists or that is formatted in your UITableViewCells is important.  One example would be if you’re loading data from JSON via a dictionary, null objects serialize as [NSNull class] and if you’re displaying it as text – would use [NSNull description] which would show up as “<null>” in one of your strings. Worse, you could be trying to perform some action on the NSNull object like expecting an array and could be trying to execute [NSNull length] and your app would crash. There are other reasons but I won’t go into that discussion as this post is about my findings on how to properly setup testing for UITableViewCells. Lets take a look at the UITableViewCell implementation before we look at how we can test it.

 

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
  TweetObj *tweet = [_tweets objectAtIndex:indexPath.row];
  if( tweet.type == TweetTypeImage ) {
    TweetImageTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:TweetImageTableViewCellIdentifier];
    [self configureCell:cell forIndexPath:indexPath];
    return cell;
  } else if( tweet.type == TweetTypeNormal ) {
    // do another cell
  } else {
    // another cell
  }
}
 
- (void)configureCell:(UITableViewCell *)cell forIndexPath:(NSIndexPath *)indexPath
{
  if( [cell isKindOfClass:[TweetImageTableViewCell class]] ) {
    TweetObj *tweet = [_tweets objectAtIndex:indexPath.row];
    NSString *firstName = [tweet.data objectForKey:@"first_name"];
    NSString *lastName = [tweet.data objectForKey:@"last_name"];
    NSString *name = [NSString stringWithFormat:@"%@ %@", firstName, lastName];
    [((TweetImageTableViewCell *)cell).nameLabel setText:name];
 
  } else if ( [cell isKindOfClass:SomeOtherTableViewCell class]] ) {
    // do other
  }
}

 

My first approach using OCMock

OCMock is great.  It can be used for mocking xibs and stubbing objects that return pre-determined values for specific method invocations and can verify interaction patterns. Naturally I thought about using it for unit testing a table view cell. I couldn’t get it working (and I’ll get to why) but here is the test I wrote out that would expect to work.

 

@implementation TableViewCellTests
 
  - (void)setUp
  {
    _controller = [[MySampleViewController alloc] init];
    _tableViewMock = [OCMockObject niceMockForClass:[UITableView class]];
    [_tableViewMock registerNib:[UINib nibWithNibName:@"MyTableViewCell" bundle:nil] forCellReuseIdentifier:MyTableViewCellIdentifier];
  }
 
    - (void)testTweetImageCell
    {
        TweetObj *tweet = [[TweetObj alloc] init];
        tweet.type = TweetTypeImage;
        tweet.data = [NSMutableDictionary dictionaryWithDictionary:@{ @"first_name" : @"firstname", @"last_name" : @"lastname" }];
        _mockTweets = [NSMutableArray arrayWithObject:tweet];
        [_controller setValue:_mockTweets forKey:@"_tweets"];
 
        NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
        [[[_tableViewMock expect] andReturn:[[[NSBundle mainBundle] loadNibNamed:@"TweetImageTableViewCell" owner:self options:nil] lastObject]] dequeueReusableCellWithIdentifier:MyTableViewCellIdentifier forIndexPath:indexPath];
 
        TweetImageTableViewCell *cell = (TweetImageTableViewCell *)[_controller tableView:_tableViewMock cellForRowAtIndexPath:indexPath];
        STAssertNotNil( cell, @"should not be nil" );
        STAssertTrue( [cell.nameLabel.text isEqualToString:@"firstname lastname"], @"should be equal" );
        [_tableViewMock verify];
    }
 
@end

If your’e familiar with OCMock, the test mockup looks legit. If you set breakpoints it’ll even go though configureCell:forIndexPath. The problem is isKindOfClass. The cell doesn’t match any of the cells in the conditional statement and skips over any of the cell rendering so the test fails. I also tried:

id mockCell = [OCMockObject partialMockForObject:[[[NSBundle mainBundle] loadNibNamed:@"MyTableViewCell" owner:self options:nil] lastObject]];
[[[mockCell stub] andReturnValue:OCMOCK_VALUE((BOOL) {YES})] isKindOfClass:[OCMConstraint isKindOfClass:[MyTableViewCell class]]];
[[[_tableViewMock expect] andReturn:mockCell] dequeueReusableCellWithIdentifier:MyTableViewCellIdentifier forIndexPath:indexPath];

After lots and lots of googling, OCMock uses NSProxy to mock and stub objects and isKindOfClass isn’t going to work here.

So what are our options? What we’re trying to test is the data transformation and not necessarily the rendering of the table view cell. If we refactor this and apply a controller to manipulate the data, we can now unit test it without using OCMock.

@interface TweetModelController : NSObject
 
- (instancetype)initWithArray:(NSArray *)model;
- (NSString *)nameAtIndexPath:(NSIndexPath *)indexPath;
 
@end
 
@interface TweetModelController() {
  NSArray *_model;
}
 
@end
 
@implementation @TweetModelController
 
- (instancetype)initWithArray:(NSArray *)model
{
  if( self = [super init] ) {
    _model = [model copy];
  }
  return self;
}
 
- (NSString *)nameAtIndexPath:(NSIndexPath *)indexPath
{
  if( [_model count] &gt; indexPath.row ) {
    NSString *name = //build the name string
    return name;
  }
}
@end

Now lets see this implemented in the table view cell.

- (void)viewDidLoad
{
  [super viewDidLoad];
  // initalize data model
  _modelController = [[TweetModelController alloc] initWithArray:_tweets];
}
 
- (void)configureCell:(UITableViewCell *)cell forIndexPath:(NSIndexPath *)indexPath
{
  if( [cell isKindOfClass:[TweetImageTableViewCell class]] ) {
    [((TweetImageTableViewCell *)cell).nameLabel setText:[_modelController nameAtIndexPath:indexPath];
 
  } else if ( [cell isKindOfClass:SomeOtherTableViewCell class]] ) {
    // do other
  }
}

The key thing to remember is remember all data manipulation should now occur in the controller, if we don’t then we won’t be able to have tests.

@implementation TableViewCellTests
 
- (void)setUp
{
  Tweet *tweet = [[Tweet alloc] init];
  tweet.type = TweetTypeImage;
  tweet.data = [NSMutableDictionary dictionaryWithDictionary:@{ @"first_name" : @"firstname", @"last_name" : @"lastname" }];
  _stubTweets = [NSMutableArray arrayWithObject:tweet];
  _controller = [[TweetModelController alloc] initWithArray:_stubTweets];
}
 
- (void)testTweetImageCell
{
  NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
  NSString *testString = [_controller nameAtIndexPath:indexPath];
  STAssertTrue( [testString isEqualToString:@"firstname lastname"] );
}
@end

Refactoring so your code can be more testable is a very common practice and in this case it was the right solution.