2

I am trying to write unit tests for a view controller that implements the NSFetchedResultsControllerDelegate protocol. The first test of the implementation (after some other tests of this view controller) is to verify that a table row is inserted into the table view when a new object is inserted.

My first implementation of the test was:

- (void) setUp {
    [super setUp];

    sut = [[JODataTableViewController alloc] init];
    fetchedResultsCtrlrMock = [OCMockObject niceMockForClass:[NSFetchedResultsController class]];
    NSError *__autoreleasing *err = (NSError *__autoreleasing *) [OCMArg anyPointer];
    [[[fetchedResultsCtrlrMock expect] andReturnValue:OCMOCK_VALUE((BOOL){YES})] performFetch:err];
    [sut setValue:fetchedResultsCtrlrMock forKey:@"fetchedResultsController"];
    [sut view]; // This invokes viewDidLoad.
}

- (void) tearDown {
    sut = nil;

    [super tearDown];
}

- (void) testObjectInsertedInResultsAddsARowToTheTable {
    NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
    id tableViewMock = [OCMockObject mockForClass:[UITableView class]];
    sut.tableView = tableViewMock;
    [[tableViewMock expect] insertRowsAtIndexPaths:@[indexPath]
                                  withRowAnimation:UITableViewRowAnimationLeft];

    [sut controller:nil didChangeObject:nil
        atIndexPath:nil
      forChangeType:NSFetchedResultsChangeInsert
       newIndexPath:indexPath];

    [tableViewMock verify];
}

When it tried to implement the functionality in the view controller to move to the green status (TDD), I wrote the following code:

- (void) controllerWillChangeContent:(NSFetchedResultsController *)controller {
    [self.tableView beginUpdates];
}


- (void) controller:(NSFetchedResultsController *)controller
    didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath
      forChangeType:(NSFetchedResultsChangeType)type
       newIndexPath:(NSIndexPath *)newIndexPath {
    UITableViewCell *cell;

    switch (type) {
        case NSFetchedResultsChangeInsert:
            [self.tableView insertRowsAtIndexPaths:@[newIndexPath]
                                  withRowAnimation:UITableViewRowAnimationLeft];
            break;

    }
}


- (void) controllerDidChangeContent:(NSFetchedResultsController *)controller {
    [self.tableView endUpdates];
}

However, I couldn't get it to pass and the error is:

Test Case '-[JODataTableViewControllerTests testObjectInsertedInResultsAddsARowToTheTable]' started.
Unknown.m:0: error: -[JODataTableViewControllerTests testObjectInsertedInResultsAddsARowToTheTable] : OCMockObject[UITableView]: unexpected method invoked: isKindOfClass:<??> 
Test Case '-[JODataTableViewControllerTests testObjectInsertedInResultsAddsARowToTheTable]' failed (0.001 seconds).

I tried to add one or more times the following line to the preparation part of the test with the same results.

[[[tableViewMock expect] andReturnValue:OCMOCK_VALUE((BOOL){YES})] isKindOfClass:[OCMArg any]];

As you can see, I am currently using OCUnit and OCMock. I would consider other tools only if it is impossible to create these kind of tests with this toolset, and in that case I would appreciate to an explanation of their limitations, should they exist.

As far as I understand, the mock is unable to "lie" about the nature of its class even when told to do so. Also, the error doesn't provide information about the class UITableView is looking for. I know that it isn't a good practice for testing to use -isKindOfClass:, but it isn't my code.

Thank you for your help.

Jon Reid
  • 20,545
  • 2
  • 64
  • 95
Jorge Ortiz
  • 1,556
  • 1
  • 16
  • 19

2 Answers2

0

I've seen failures related to isKindOfClass: calls before, and they're usually the result of the way Apple implemented a certain feature in their own code. The standard mockForClass: will reject any unexpected messages as a failing situation. A simple solution is to switch your mock to niceMockForClass: which is forgiving about such unexpected messages.

Adding expectations for messages coming from third-party code would make your tests very coupled to external implementation details. Making sure isKindOfClass: gets called is clearly not an explicit requirement of your system.

Greg Haskins
  • 6,714
  • 2
  • 27
  • 22
  • Thank you for your idea. Funny enough, I use the nice mock, the test fails because, somehow, tableView becomes nil in the view controller (everything remains as in the question). – Jorge Ortiz Apr 22 '13 at 13:57
  • I also agree with your comment on the coupling. I am worried about this also. Is there any better way to implement the test? – Jorge Ortiz Apr 22 '13 at 13:59
  • 1
    What if you create a UITableView and partially mock it? – Ben Flynn Apr 24 '13 at 03:13
  • @BenFlynn This seems to work. I would be glad to accept your answer, if you write it as such. Thank you! – Jorge Ortiz Apr 24 '13 at 13:50
0

One way to work around mocking objects that have concealed inner behaviors is to use a partial mock. In your case:

id tableViewMock = [OCMock partialMockForObject:[[UITableView alloc] init]];

When I do this I usually change the name slightly and would call it tableViewPartial

Ben Flynn
  • 18,524
  • 20
  • 97
  • 142