Question

I'm trying to mock and test UITableViewCells to make sure my configureCell:forIndexPath works correctly, except I can't get it to work using isKindOfClass but only conformsToProtocol. This would require all of my uitableviewcells to have it's own protocol and does not seem needed.

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 
{
  FeedObj *item = [_feedElements objectAtIndex:indexPath.row];
  if( item.obj_type == FeedObjTypeFriendAdd ) {
    MyTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:MyTableViewCellIdentifier forIndexPath:indexPath];
    [self configureCell:cell forIndexPath:indexPath]
    return cell;
  } else if( item.obj_type = FeedObjTypeSomeOtherType ) {
    // do another cell
  }
}

- (void)configureCell:(UITableViewCell *)cell forIndexPath:(NSIndexPath *)indexPath 
{
  // only enters conditional in test if I do [cell conformsToProtocol:@protocol(SomeIndividualProtocolForEachTableViewcell)]                  
  if( [cell isKindOfClass:[MyTableViewCell class]] ) {
    // do the configuring
    FeedObj *item = [_streamElements objectAtIndex:indexPath.row];

    NSString *firstName = [item.obj_data objectForKey:@"first_name"];
    NSString *lastName = [item.obj_data objectForKey:@"last_name"];
    NSString *name = [NSString stringWithFormat:@"%@ %@.", firstName, [lastName substringToIndex:1]];
    NSString *text = [NSString stringWithFormat:@"%@ has joined", name];

    [((MyTableViewCell *)cell).messageLabel setText:text];

  } else if( [cell isKindOfClass[SomeOtherTableView class]] ) {
    // do other config
  }
}


    @implementation SampleTests
    - (void)setUp
    {
        _controller = [[MySampleViewController alloc] init];
        _tableViewMock = [OCMockObject niceMockForClass:[UITableView class]];
        [_tableViewMock registerNib:[UINib nibWithNibName:@"MyTableViewCell" bundle:nil] forCellReuseIdentifier:MyTableViewCellIdentifier];
    }

    - (void)testFriendAddCell
    {
        FeedObj *friendAdd = [[FeedObj alloc] init];
        friendAdd.obj_type = FeedObjTypeFriendAdd;
        friendAdd.obj_data = [NSMutableDictionary dictionaryWithDictionary:@{ @"first_name" : @"firstname", @"last_name" : @"lastname" }];
        _mockStreamElements = [NSMutableArray arrayWithObject:friendAdd];
        [_controller setValue:_mockStreamElements forKey:@"_feedElements"];

        NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
        [[[_tableViewMock expect] andReturn:[[[NSBundle mainBundle] loadNibNamed:@"MyTableViewCell" owner:self options:nil] lastObject]] dequeueReusableCellWithIdentifier:MyTableViewCellIdentifier forIndexPath:indexPath];

        MyTableViewCell *cell = (MyTableViewCell *)[_controller tableView:_tableViewMock cellForRowAtIndexPath:indexPath];
        STAssertNotNil( cell, @"should not be nil" );
        STAssertTrue( [cell.messageLabel.text isEqualToString:@"firstname l. has joined"], @"should be equal" );
        [_tableViewMock verify];
    }
    @end

I've also tried doing [[[mockCell stub] andReturnValue:OCMOCK_VALUE((BOOL) {YES})] isKindOfClass:[MyTableViewCell class]]] with a mockCell expect and it doesn't work either. Like this:

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];

I even tried with an OCMConstraint listed in http://blog.carbonfive.com/2009/02/17/custom-constraints-for-ocmock/.

Is there anyway to do this or do I have to use protocols for each tableviewcell? Thanks in advance

Was it helpful?

Solution

I'd strongly suggest you rethink how you are building out this implementation. For starters, a view controllers is great at managing a view, but managing your model data is not what it's for. Its good for passing around your model data to the views it manages, so with that in mind, let's build this out like that.

Let's start by introducing a new class, called FeedController. This controller's job is to sit between your VC and your model data backing this screen. Let's assume this public interface:

@interface FeedController : NSObject
- (instancetype)initWithFeedArray:(NSArray *)array;
- (NSString *)firstNameAtIndexPath:(NSIndexPath *)path;
- (NSString *)lastNameAtIndexPath:(NSIndexPath *)path;
- (NSString *)fullNameAtIndexPath:(NSIndexPath *)path;
// This should probably have a better name
- (NSString *)textAtIndexPath:(NSIndexPath *)path;
@end

I'm not going to implement these methods, but they'd look exactly like you'd expect. The initializer would copy the array passed into it, store it in an ivar, and the other methods would take the piece of info out of the array at the specific index and apply any custom transformations you must (like combining the first and last name to get the full name). The main goal here is to transfer the data, not manipulate it. The moment you try and manipulate this data in your view controller, is the moment you'll be back to square one, testing wise.

The object of your configureCell:forIndexPath: is now just to transfer data from the FeedController class, which is infinitely simple to test. No need to set up a responder chain, mock out objects, or anything. Just supply some fixture data and away you go.

You are still testing the pieces that make up your configureCell:forIndexPath: but not directly testing that method anymore. If you want to make sure that the view is being populated correctly, great, you should. However, you'll do this differently, this isn't a job for unit tests. Pull out UIAutomation or your favourite UI testing framework, and test your UI. Use the unit tests on the FeedController to test your data and transformations.

I hope this helps.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top