(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"];
}
MKTArgumentCaptor
. With this approach, the helper method I'd extract comes after the call toreloadTableViewContents
not before, so instead ofsetUpFakeNetworkFetcherToSucceedWithResult:
I'd probably call itsimulateNetworkFetcherSucceedingWithResult:
– Monafo