Some validation using block methods and OCMockito

I use 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 would like to make fun of fetchInfo: so that it returns an array of dummy results without getting into the network and checks that the reloadData method reloadData been called in a UITableView , and the model is what it should be.

Since this code is asynchronous, I assume that I have to somehow capture the block and call it manually from my tests.

How can i do this?

+6
source share
2 answers

It is pretty simple:

 - (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 the creation of mockedFetcher and mockedTable to setUp save your lines of similar code in other tests.

+5
source

( 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"]; } 
+4
source

Source: https://habr.com/ru/post/969841/


All Articles