Some verification involving block methods and OCMockito

922 Views Asked by At

I'm using OCMockito and I want to test a method in my ViewController that uses a NetworkFetcher object and a block:

- (void)reloadTableViewContents
{
    [self.networkFetcher fetchInfo:^(NSArray *result, BOOL success) {
        if (success) {
            self.model = result;
            [self.tableView reloadData];
        }
    }];
}

In particular, I'd want to mock fetchInfo: so that it returns a dummy result array without hitting the network, and verify that the reloadData method was invoked on the UITableView and the model is what it should be.

As this code is asynchronous, I assume that I should somehow capture the block and invoke it manually from my tests.

How can I accomplish this?

2

There are 2 best solutions below

1
On

(Edit: See Eugen's answer, and my comment. His use of OCMockito's MKTArgumentCaptor not only eliminates the need for the FakeNetworkFetcher, but results in a better test flow that reflects the actual flow. See my Edit note at the end.)

Your real code is asynchronous only because of the real networkFetcher. Replace it with a fake. In this case, I'd use a hand-rolled fake instead of OCMockito:

@interface FakeNetworkFetcher : NSObject
@property (nonatomic, strong) NSArray *fakeResult;
@property (nonatomic) BOOL fakeSuccess;
@end

@implementation FakeNetworkFetcher

- (void)fetchInfo:(void (^)(NSArray *result, BOOL success))block {
    if (block)
        block(self.fakeResult, self.fakeSuccess);
}

@end

With this, you can create helper functions for your tests. I'm assuming your system under test is in the test fixture as an ivar named sut:

- (void)setUpFakeNetworkFetcherToSucceedWithResult:(NSArray *)fakeResult {
    sut.networkFetcher = [[FakeNetworkFetcher alloc] init];
    sut.networkFetcher.fakeSuccess = YES;
    sut.networkFetcher.fakeResult = fakeResult;
}

- (void)setUpFakeNetworkFetcherToFail
    sut.networkFetcher = [[FakeNetworkFetcher alloc] init];
    sut.networkFetcher.fakeSuccess = NO;
}

Now your success path test needs to ensure that your table view is reloaded with the updated model. Here's a first, naive attempt:

- (void)testReloadTableViewContents_withSuccess_ShouldReloadTableWithResult {
    // given
    [self setUpFakeNetworkFetcherToSucceedWithResult:@[@"RESULT"]];
    sut.tableView = mock([UITablewView class]);

    // when
    [sut reloadTableViewContents];

    // then
    assertThat(sut.model, is(@[@"RESULT"]));
    [verify(sut.tableView) reloadData];
}

Unfortunately, this doesn't guarantee that the model is updated before the reloadData message. But you'll want a different test anyway to ensure that the fetched result is represented in the table cells. This can be done by keeping the real UITableView and allowing the run loop to advance with this helper method:

- (void)runForShortTime {
    [[NSRunLoop currentRunLoop] runUntilDate:[NSDate date]];
}

Finally, here's a test that's starting to look good to me:

- (void)testReloadTableViewContents_withSuccess_ShouldShowResultInCell {
    // given
    [self setUpFakeNetworkFetcherToSucceedWithResult:@[@"RESULT"]];

    // when
    [sut reloadTableViewContents];

    // then
    [self runForShortTime];
    NSIndexPath *firstRow = [NSIndexPath indexPathForRow:0 inSection:0];
    UITableViewCell *firstCell = [sut.tableView cellForRowAtIndexPath:firstRow];
    assertThat(firstCell.textLabel.text, is(@"RESULT"));
}

But your real test will depend on how your cells actually represent the fetched results. And that shows that this test is fragile: if you decide to change the representation, then you have to go fix up a bunch of tests. So let's extract a helper assertion method:

- (void)assertThatCellForRow:(NSInteger)row showsText:(NSString *)text {
    NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:0];
    UITableViewCell *cell = [sut.tableView cellForRowAtIndexPath:indexPath];
    assertThat(cell.textLabel.text, is(equalTo(text)));
}

With that, here's a test that uses our various helper methods to be expressive and pretty robust:

- (void)testReloadTableViewContents_withSuccess_ShouldShowResultsInCells {
    [self setUpFakeNetworkFetcherToSucceedWithResult:@[@"FOO", @"BAR"]];

    [sut reloadTableViewContents];

    [self runForShortTime];
    [self assertThatCellForRow:0 showsText:@"FOO"];
    [self assertThatCellForRow:1 showsText:@"BAR"];
}

Note that I didn't have this end in my head when I started. I even made some false steps along the way which I haven't shown. But this shows how I try to iterate my way to test designs.

Edit: I see now that with my FakeNetworkFetcher, the block get executed in the middle of reloadTableViewContents — which doesn't reflect what will really happen when it's asynchronous. By shifting to capturing the block then invoking it according to Eugen's answer, the block will be executed after reloadTableViewContents completes. This is far better.

- (void)testReloadTableViewContents_withSuccess_ShouldShowResultsInCells {
    [sut reloadTableViewContents];
    [self simulateNetworkFetcherSucceedingWithResult:@[@"FOO", @"BAR"]];

    [self runForShortTime];
    [self assertThatCellForRow:0 showsText:@"FOO"];
    [self assertThatCellForRow:1 showsText:@"BAR"];
}
1
On

This is quite easy:

- (void) testDataWasReloadAfterInfoFetched 
{
    NetworkFetcher mockedFetcher = mock([NetowrkFetcher class]);
    sut.networkFetcher = mockedFetcher;

    UITableView mockedTable = mock([UITableView class]);
    sut.tableView = mockedTable;

    [sut reloadTableViewContents];

    MKTArgumentCaptor captor = [MKTArgumentCaptor new];
    [verify(mockedFetcher) fetchInfo:[captor capture]];

    void (^callback)(NSArray*, BOOL success) = [captor value];

    NSArray* result = [NSArray new];
    callback(result, YES);

    assertThat(sut.model, equalTo(result));
    [verify(mockedTable) reloadData];
}

I put everything in one test method but moving creation of mockedFetcher and mockedTable to setUp will save you lines of similar code in other tests.