When I started testing iOS apps, which shamefully was not when I started writing iOS apps, I discovered the biggest impediment to thorough testing on iOS was the View Controller, and it’s mix of UI code, and not.
Now I’m working on my first independent app (yay!) of course I am writing extensive unit tests.
I won’t go into mocking here, but you need a mocking framework and some understanding of what mocking is for this to make sense. Currently, I’m using OCMock. Also, XCTest is not the best documented, and here is a handy list of asserts.
Introducing The Presenter
Step 1 for writing thorough unit tests is getting all the non-view code out of the ViewController. This goes in the Presenter. So for each View Controller, I know have MyClassViewController and MyClassPresenter.
Over time I have refined this and now I have at a top level “ViewController” and “Presenter” classes. The Presenter knows what ViewController it has, but the MyClassViewController knows nothing, the ViewController merely knows there is a Presenter, and can call some standard methods – viewLoaded:, leftNavigationButtons:, rightNavigationButtons:.
This could be a blog post all on it’s own (maybe it will be soon!) but the point is: get non-view code out of the ViewController.
This is handy for testing that the right thing happens when a button is pressed. This can be done using performSelector:
For example, if we want to verify that the first and only left navigation button does what we want it to:
// Extract the button. UIBarButtonItem *button = (UIBarButtonItem *) [[presenter_ leftNavigationButtons] firstObject]; // Perform the action. [[button target] performSelector:[button action] withObject:button];
Partial Mock For Object – Woah
I discovered this via StackOverflow and it’s genius. One thing that I want to mock and verify when testing my ViewController is the navigationController property. But it’s up in the super class, and the trick of setValue:forKey: was not working.
So what you can do, is create a partialMockForClass, and then stub the navigationController property. (Here, I’m making sure that the ViewController dismiss… method for use by the presenter, causes the navigationController method to be called)
// Create the mock navigation controller. id mockNavigationController = OCMStrictClassMock([UINavigationController class]); // Create a partial mock for the ViewController. id mockViewController = [OCMockObject partialMockForObject:viewController_]; // Stub to return mockNavigationController. [[[mockViewController expect] andReturn:mockNavigationController] navigationController]; // Set up expectations. OCMExpect([mockNavigationController dismissViewControllerAnimated:YES completion:nil]); // Call the method that should trigger them. [viewController_ dismissViewControllerAnimated:YES withCompletionBlock:nil]; // Verify! OCMVerify([mockNavigationController dismissViewControllerAnimated:YES completion:nil]);
♥ Happy Unit Testing ♥
For more detail on this and other aspects of iOS unit-testing you might find my digital workshop helpful.