( Edit: See Eugen's answer and my comment. Using OCMockito MKTArgumentCaptor not only eliminates the need for a FakeNetworkFetcher , but it also gives a better test stream, reflecting the actual stream. See My โEditโ note at the end.)
Your real code is asynchronous just because of the real networkFetcher . Replace it with a fake. In this case, instead of OCMockito, I used manual fake:
@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 assume that your test system is in the test fixture as 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; }
Your success path test should now reload the table view with the updated model. Here is the 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 does not guarantee that the model is updated to the reloadData message. But in any case, you will need another test to make sure that the result is presented in the table cells. This can be done by preserving the real UITableView and letting the run loop progress with this helper method:
- (void)runForShortTime { [[NSRunLoop currentRunLoop] runUntilDate:[NSDate date]]; }
Finally, here is a test that starts 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 actual test will depend on how your cells actually represent the results. And it shows that this test is fragile: if you decide to change the view, then you need to fix a lot of tests. Therefore, give the opportunity to extract the helper confirmation 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))); }
At the same time, here a test that uses our various auxiliary methods is expressive and quite reliable:
- (void)testReloadTableViewContents_withSuccess_ShouldShowResultsInCells { [self setUpFakeNetworkFetcherToSucceedWithResult:@[@"FOO", @"BAR"]]; [sut reloadTableViewContents]; [self runForShortTime]; [self assertThatCellForRow:0 showsText:@"FOO"]; [self assertThatCellForRow:1 showsText:@"BAR"]; }
Please note that I did not have this goal in my head when I started. I even took a few false steps along a path that I did not show. But this shows how I try to repeat my path for testing projects.
Edit: Now I see that with my FakeNetworkFetcher, the block is executed in the middle of reloadTableViewContents - which does not reflect what really happens when it is asynchronous. Having moved to capture the block, calling it in accordance with Eugeneโs response, the block will be executed after reloadTableViewContents completed. This is much better.
- (void)testReloadTableViewContents_withSuccess_ShouldShowResultsInCells { [sut reloadTableViewContents]; [self simulateNetworkFetcherSucceedingWithResult:@[@"FOO", @"BAR"]]; [self runForShortTime]; [self assertThatCellForRow:0 showsText:@"FOO"]; [self assertThatCellForRow:1 showsText:@"BAR"]; }