Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to Unit Test a UICollectionView within a UIViewController

I want to unit test a UICollectionView which is inside a UIViewController (e.g. I want to test that the UICollectionView has the number of cells that I am expecting it to have in my unit test)

My unit test is based on the following blog (on how to unit test a view controller): http://yetanotherdevelopersblog.blogspot.co.il/2012/03/how-to-test-storyboard-ios-view.html

In the unit test I am able to get a pointer to the UICollectionView in the UIViewController which I am testing. Here is the code in my test's setUp method:

storyboard = [UIStoryboard storyboardWithName:@"MainStoryboard" bundle:nil];
viewController = [self.storyboard instantiateViewControllerWithIdentifier:@"MainViewController"];
[viewController performSelectorOnMainThread:@selector(loadView) withObject:nil waitUntilDone:YES];
self.collectionView = viewController.collectionView

Unfortunately, there are no cells in this UICollectionView (i.e. self.collectionView.visibleCells.count equals 0, hence I cannot access the cells and test that the data is what I am expecting it to be), although when debugging I can see that my application add those cells to the collection view.

What am I doing wrong? Any tips & tricks on how to unit test UICollectionView within a UIViewController?

Edit: After Roshan's help I can now narrow down my problem. I am unit testing a UIViewController that has a UICollectionView. In my unit test I want to test that values of the cells inside CollectionView is what I am expecting it to be. But, collectionView:cellForItemAtIndexPath: is not being invoked on my ViewController and hence my cells doesn't exist when this is invoked as a unit test. Any idea?

like image 797
nirch Avatar asked Oct 27 '13 14:10

nirch


2 Answers

When implementing TTD on iOS, try not to rely in the system calling the delegate and data source methods.

You should be calling these methods directly from your unit tests. It's just a matter then of configuring the right environment for your tests.

For example, when I implement TDD for a UICollectionView, I create two separate classes specifically to implement the UICollectionViewDataSource and UICollectionViewDelegate protocols creating a separation of concerns and I can unit test these classes separately to the view controller itself, although I still need to initialize the view controller to setup the view hierarchy.

Here's an example, headers and other minor pieces of code omitted of course.

UICollectionViewDataSource example

@implementation CellContentDataSource

@synthesize someModelObjectReference = __someModelObjectReference;

#pragma mark - UICollectionViewDataSource Protocol implementation

- (NSInteger) collectionView:(UICollectionView *)collectionView
  numberOfItemsInSection:(NSInteger)section
{
    return __someModelObjectReference ? 
         [[__someModelObjectReference modelObjects] count] : 0;
}

- (UICollectionViewCell *) collectionView:(UICollectionView *)collectionView
               cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString * const kCellReuseIdentifer =   @"Cell";
    UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:kCellReuseIdentifer forIndexPath:indexPath];

    ModelObject *modelObject = [__someModelObjectReference modelObjects][[indexPath item]];

    /* Various setter methods on your cell with the model object */

    return cell;
}
@end

Unit Test Example

- (void) testUICollectionViewDataSource
{
    UIStoryBoard *storyboard = [UIStoryboard storyboardWithName:@"MainStoryboard" bundle:nil];
    MainViewController *viewController = [storyboard instantiateViewControllerWithIdentifier:@"MainViewController"];
    [viewController view]; // Loads the view hierarchy

    // Using OCMock to mock the returning of the model objects from the model object reference.
    // The helper method referenced builds an array of test objects for the collection view to reference
    id modelObjectMock = [OCMockObject mockForClass:[SomeModelObjectReference class]];
    [[[modelObjectMock stub] andReturn:[self buildModelObjectsForTest]] modelObjects];

    CellContentDataSource *dataSource = [CellContentDataSource new];
    dataSource.someModelObjectReference = modelObjectMock;
    viewController.collectionView.dataSource = dataSource;

    // Here we call the data source method directly
    UICollectionViewCell *cell = [dataSource collectionView:viewController.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]];

    XCTAssertNotNil(cell, @"Cell should not be nil");
    // Assert your cells contents here based on your test model objects
}
like image 154
Fergal Rooney Avatar answered Oct 10 '22 00:10

Fergal Rooney


I don't know exactly where the problem is but Apple's documentation says this about loadView:

You should never call this method directly. The view controller calls this method when the view property is requested but is currently nil. If you create your views manually, you must override this method and use it to create your views. If you use Interface Builder to create your views and initialize the view controller—that is, you initialize the view using the initWithNibName:bundle: method, set the nibName and nibBundle properties directly, or create both your views and view controller in Interface Builder—then you must not override this method.

So, instead of calling it directly, call [viewController view] to force the view to be loaded.

As to your problem, check if viewController.collectionView is 0 or not. It is possible that your outlet has not yet been set.

like image 34
Roshan Avatar answered Oct 09 '22 23:10

Roshan