0

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

  • 1
    What is configureCell method definition? Does it use MyTableViewCell* for the passed cell? – Phil Wilson Sep 29 '13 at 13:40
  • Is MyTableViewCell definitely a subclass of UITableViewCell? – Phil Wilson Sep 29 '13 at 13:48
  • I've added the configureCell method definition and yes MyTableViewCell is definitely a sublcass of UITableViewCell. I'm using a xib and I extend UITableViewCell and set the class name in the xib to MyTableViewCell – user2828419 Sep 30 '13 at 04:08
  • Would you be able to show your test case method, and explain more about what "doesn't work"? – Carl Veazey Sep 30 '13 at 04:13
  • @CarlVeazey I've added the test case method updated the code on where the test doesn't enter the conditional statement for isKindOfClass. If using isKindOfClass, the cell does return not nil, but the label is filled in with the default value of the cell, rather than the actual test first and last name. Everything passes if I use conformsToProtocol. – user2828419 Sep 30 '13 at 04:48
  • Ah, I see - you're not mocking the cells themselves, but mocking the table view and having it return real cells, and doing assertions on their properties. What protocol are you testing for, though, when you say if you use `conformsToProtocol:` the tests pass? – Carl Veazey Sep 30 '13 at 04:54
  • I'm totally stumped, I'm able to get pretty similar code to work OK, when you put a breakpoint in `configureCell:forIndexPath:`, what does it say when you type `po [cell class]`? – Carl Veazey Sep 30 '13 at 05:42
  • I've added a protocol with the same exact name as the MyTableViewCell name and it works. po [cell class] returns me MyTableViewCell, and it still doesn't go into the conditional statement. I've set that breakpoint many times. Also, I've even tried mocking the cell too. I'm updating the post with what I've also tried – user2828419 Sep 30 '13 at 15:34
  • To be clear, when you run this *outside* of the unit test target, does the method behave as you'd expect? – Carl Veazey Oct 01 '13 at 04:03

1 Answers1

1

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.

jer
  • 20,094
  • 5
  • 45
  • 69