IMPROVING IOS UNIT TESTS WITH OCMOCK
By: Andy Obusek
http://engineering.aweber.com/improving-ios-unit-tests-with-ocmock/
OCMock's website: http://ocmock.org/
CocoaPod: https://github.com/CocoaPods/Specs/tree/master/OCMock
GitHub page: https://github.com/obuseme/OCMockSample
CocoaPod using guide: https://guides.cocoapods.org/using/using-cocoapods.html
With each new release of XCode and iOS, Apple is continually providing better support for the automated testing of iOS applications. One piece to the puzzle that is still missing from Apple's toolbox is a mock object framework. That's okay though, OCMock is a mature and open source called Objective-C implementation of mock objects. OCMock's website is a great resource for learning this tool that includes a features page, tutorials, a link to the Github repository, instructions for setting it up in XCode, and binaries for download. My preferred installation approach is through an available CocoaPod.
Rather than review the basics of OCMock, which it's website already does very well, this article will overview some tricks and techniques for pulling more power out of OCMock.
Sample code for this post can be found on my GitHub page.
Partial mock the object under test to stub out internal methods with expected responses
An OCMock feature that I find myself using frequently are partial mocks. Specifically, try creating a partial mock of the object for which you're writing a test. You'll gain flexibility by having more fine grain control of the behavior of other method calls from within the method under test.
Partial mocks, in OCMock, provide the ability to use an actual instance of the partially mocked object, while also being able to define mock behavior for the methods you designate. This is different from regular mock objects. Regular mock objects are just a dumb shell that do exactly what you tell them, where partial mocks maintain all behavior of the underlying object unless you define otherwise. Partial mocks are created from an object, not a class (where regular mocks are created from a class).
In this example, we have a class representing a Person, with a couple properties and methods for processing the Person's name:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
@interface Person() @property (nonatomic, strong) NSString *firstName; @property (nonatomic, strong) NSString *lastName; @property (nonatomic, strong) NSString *suffix; @end
@implementation Person - (NSString *)getFullName { return [NSString stringWithFormat:@"%@ %@", self.firstName, self.lastName]; } - (NSString *) getProperName { return [NSString stringWithFormat:@"%@, %@", [self getFullName], self.suffix]; } @end |
Thinking about the unit test strategy for these methods, it's easy to figure out how to write a test for getFullName
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
- (void)testGetFullName { //Create a Person to Test Person *aPersonToTest = [[Person alloc] init];
//Create two strings for the Person's first and last name NSString *firstName = @"George"; NSString *lastName = @"Bush";
//Assign the names to the Person aPersonToTest.firstName = firstName; aPersonToTest.lastName = lastName;
//Create a NSString that is the expected format of the full name NSString *expectedFullName = [NSString stringWithFormat:@"%@ %@", firstName, lastName];
//Assert that getFullName returns the full name in the format we expect XCTAssertEqualObjects([aPersonToTest getFullName], expectedFullName, @"Full name should be the first name followed by the last name"); } |
On the other hand, looking at the method getProperName, it would be useful if we could manually control the result of getFullName in order to ensure a known return value in order to make an assertion on the result of the string concatenation for the proper name. This is where partial mock objects can help.
In the following code sample, there are two important takeaways: notice that mockPersonToTest is created by passing an instantiated Personobject to the method [OCMockObject partialMockForObject:], and look at the methods called on the mock object.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
- (void)testGetProperName { //Create a Person to Test Person *aPersonToTest = [[Person alloc] init];
//Create the partial mock from the instantiated Person id mockPersonToTest = [OCMockObject partialMockForObject:aPersonToTest]; [[[mockPersonToTest stub] andReturn:@"George Bush"] getFullName];
aPersonToTest.suffix = @"Sr.";
//Call getProperName on the mock object. XCTAssertEqualObjects([mockPersonToTest getProperName], @"George Bush, Sr.", @"Proper name should be the first name followed by the last name, and then the suffix"); } |
See how getFullName is defined as a stubbed method on Person? This means that the mock object will always return the defined value, "George Bush" whenever the method getFullName is called. During testing this helps our test become more cohesive in that we focus our assertion on the result of getProperName, rather than also worrying about knowing (and really testing) the implementation details ofgetFullName inside of the test for getProperName. Instead, the test just relies on the stubbed method to return a value for getFullName, for which the suffix should be appended.
As a side note, one could argue whether your test arrives at this same approach if you're following test driven development, since maybe one would instead populate values in the properties for the name, and then only verify that the resulting proper name is of the right format (and thus never worry about whether -getFullName was called within the test), but we'll leave that point open for a future post.
While this example is trivial, it's easily imagined how partial mock objects can be used in more complicated situations in which a method under test relies upon other methods in the same class. Partial mocks are useful to ensure consistent behavior for more complicated methods that may be called within your tests.
To bring the complexity up a notch, here's an example of partial mocking the UIApplication class. UIApplication provides a very useful method, openURL which opens the provided URL in a different application. The triggered application is determined by the scheme of the provided url, so for example, "http:" is opened in Safari. Applications can register schemes that they will recognize. Another example would be the "itms-apps:" scheme which is registered by the App Store app. You can use this scheme in combination with [UIApplication openURL:] to open the App Store app right from your app. Unfortunately, unit testing this behavior without a mocking framework is nearly impossible. There's no way to programmatically determine openURL has been invoked or executed as desired. Partial mocks to the rescue.
Here's an example:
From ViewController.m
1 2 3 4 |
- (void)launchURL:(NSURL *)urlToOpen { [[UIApplication sharedApplication] openURL:urlToOpen]; } |
From ViewControllerTests.m
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
- (void)testOpenUrl { ViewController *toTest = [[ViewController alloc] init];
NSURL *toOpen = [NSURL URLWithString:@"http://www.google.com"];
// Create a partial mock of UIApplication id mockApplication = [OCMockObject partialMockForObject:[UIApplication sharedApplication]];
// Set an expectation that the UIApplication will be told to open the url [[mockApplication expect] openURL:toOpen];
[toTest launchURL:toOpen];
[mockApplication verify]; [mockApplication stopMocking]; } |
The important part of this example is to create partial mock of the instantiated UIApplication as returned by [UIApplication sharedApplication]. From there, set the expectation on the mock that it will be told to open our specified url. This will verify thatUIApplication is told to open our url.
Don't forget to "verify" your "expectations"
Setting expectations on mocks is really useful in that it allows you to verify, from your tests, that a designated method was called on the mock object. If the expected method was not called, an exception is raised, and the test fails. One huge "gotcha" to setting expectations on OCMock'd mock objects is the requirement to verify your mock object after the expectation should have been met. If you don't call "verify" on the mock object after the expectation should have been met, then an exception will not be raised, and you will get a falsely positive pass on the test that you executed.
Here's an example test (that incorrectly passes, and shows the requirement of using "verify" because the mock doesn't flag a failure even thoughdoSomethingElseWithTheName was not called):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
- (void)testGetProperName { //Create a Person to Test Person *aPersonToTest = [[Person alloc] init];
//Create the partial mock from the instantiated Person id mockPersonToTest = [OCMockObject partialMockForObject:aPersonToTest]; [[[mockPersonToTest stub] andReturn:@"George Bush"] getFullName];
[[mockPersonToTest expect] doSomethingElseWithTheName];
aPersonToTest.suffix = @"Sr.";
//Call getProperName on the mock object. XCTAssertEqualObjects([mockPersonToTest getProperName], @"George Bush, Sr.", @"Proper name should be the first name followed by the last name, and then the suffix");
//Without the call to verify here, the test will pass //[mockPersonToTest verify]; } |
The moral of this story is that when writing your tests, it's always a good idea to ensure that the test will fail when you think it should. Even if you aren't following a purely test driven approach, you can still gain value out of making sure your tests fail when you think they should. While writing the test, just jump over to the code under test, comment out the relevant code to make it fail, and run it again. If it doesn't actually fail, you know your test isn't doing exactly what it should, and in this case, that's usually because you didn't call verify on the mocks.
Rejection is immediate
If your mock object receives a message for a method which has neither been stubbed, nor defined as expected, an exception will be immediately raised and your test will fail. If this is undesirable for your use case, take a look at OCMock's nice mocks. Nice mock objects will not raise an exception if a method is called on the mock object for which an expectation or stub has been defined.
It is possible to identify a method on a nice mock that should never be called. You do this by designating "reject" on methods on the mock. In this case, an exception will be raised immediately, if that method is called later on within the test. At least you don't have to worry about forgetting to call verify on the mock in this case.
Even if you forget to call verify at the end of your test, reject will still cause your test to fail if the method is called. Here's an example of how rejecting behavior on mocks can be used to ensure a method is not called:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
- (void)testGetProperNameAndRejectDoSomethingElse { //Create a Person to Test Person *aPersonToTest = [[Person alloc] init];
//Create the partial mock from the instantiated Person id mockPersonToTest = [OCMockObject partialMockForObject:aPersonToTest];
[[[mockPersonToTest stub] andReturn:@"George Bush"] getFullName];
//Fail the test if this method is ever called [[mockPersonToTest reject] doSomethingElseWithTheName];
aPersonToTest.suffix = @"Sr.";
//Call getProperName on the mock object. XCTAssertEqualObjects([mockPersonToTest getProperName], @"George Bush, Sr.", @"Proper name should be the first name followed by the last name, and then the suffix"); } |
Privacy isn't guaranteed
A tough challenge to overcome when unit testing in any object oriented language is figuring out how to invoke private methods and modify or read private variables in the context of the tests, while also not unnecessarily modifying the code under test. You often hear the argument that code should be "testable" from the start, which I agree with, but when I find myself creating code purely for the sake of accessing methods and variables to expose them to the test case, I leverage something available to Objective-C, a category. In Objective-C, a category allows you to add behavior to a class without subclassing it. You can even define a category on a class from within a different class. This allows us to creatively define a category on our class under test, from within our test case class. Here's an example:
SomeObject.h
1 2 3 4 5 |
#import <Foundation/Foundation.h>
@interface SomeObject : NSObject - (void)aPublicMethod; @end |
SomeObject.m
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#import "SomeObject.h"
@implementation SomeObject - (void)aPublicMethod { //do something public }
//Notice this method is missing from the header file - (void)aPrivateMethod { //doSomethingPrivate } @end |
In our unit tests, we want to test the smallest possible unit of code, a method in this case. Specifically, aPrivateMethod is not exposed in the public interface of SomeObject, so how can we call the method to create tests around it? Here's how you can achieve that with use of an Objective-C category
SomeObjectTests.m
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
#import <XCTest/XCTest.h> #import "SomeObject.h"
// Definition of the category Test on the class SomeObject @interface SomeObject (Test) - (void)aPrivateMethod; @end
@interface SomeObjectTests : XCTestCase @property (nonatomic, strong) SomeObject *toTest; @end
@implementation SomeObjectTests
- (void)setUp { [super setUp]; self.toTest = [[SomeObject alloc] init]; }
- (void)testPrivateMethod { [self.toTest aPrivateMethod]; } @end |
Notice how cleanly we can expose the private method on SomeObjectwithout modifying SomeObject itself, or any other code that will run within the live application (another tip: ensure that your XCTestCase subclasses are only part of your Test build target, not the target that builds your app binary for the App Store).
Wrapping up, be sure to keep the goal of your test case in mind when using OCMock (or any other mocking framework in whatever language). It's very easy to slip towards creating so many mock objects that you're no longer actually testing anything besides the behavior of the mocking framework.
OCMock has definitely helped me achieve a higher level of confidence in my unit tests. Hopefully you give it a try and have the same result.