Awesome, that's a big first step in a very proactive direction towards making your app more reliable and new features easier to build. When the computer is testing to make sure that your changes and refactoring don't break existing features, your velocity skyrockets. This means shipping updates more often, with less effort, and a higher quailty experience for your users.
I'm not here to sell you on the idea of testing; you came here on your own after all. So let's just dive into the gritty details.
The iOS ecosystem and its community aren't rooted in the sort of 'testing culture' that other popular programming languages are, like Ruby or Python. Those langauges are dynamically typed, effectively meaning their source code is not statically checked by a compiler for type system level errors like the C family of languages and Swift are.
Thankfully, on iOS we have some initial assurance that our code will not unknowingly break because we changed the contract of a method; our code will simply stop compiling (most of the time, there are certainly ways you can mess this up in Objective-C, of course, using the id
type).
So, in this article we're just going to focus on testing out Business Logic(tm), or the code that makes our app do what it's supposed to do. If your app was a calculator, we would be testing that it does math correctly. If it was an app for translating English to Spanish, we could write tests confirming that some set of known input is translated correctly by the class that handles translations. This is the meat of the app.
So first let's start with something that will probably affect many of you reading this, whether you'd like to admit it or not:
By nature of the SDK provided by app, many apps suffer from view controller bloat in which a particular UIViewController subclass ends up containing a considerable amount of business logic. First, while this is problematic and generally considered a so called code smell, you should know that you are not alone here and it isn't your fault — this is a problem with so many people in the community that it's one of the most frequent topics of snarky elitist Twitter users and the source of many Hot Takes(tm) on Medium dot com. Second, this problem is generally pretty simple to solve.
Before I talk about solutions, let's take a moment to talk about why View Controller Bloat is a problem in the context of this article. When you test code, you test it in isolation. The intention is to discern whether a very particular, small subset of functionality is working as you expect it to work. But, unfortunately, because UIViewController is 1) responsible for doing just about everything in the world (in iOS 9 it's slated to add burrito creation to its repertoire of unrelated tasks) and 2) it's tied very closely to a UIView instance it becomes somewhat difficult to test code that belongs to a UIViewController subclass in isolation. You can of course (if you just said "well, actually...", just keep reading) do it, but it means 1) stubbing out the interactions with the view and 2) writing tests that are exposed to unrelated functionality of view controllers, losing our solace of isolation.
All right, spiel over. Moving on.
I'm going to present two solutions, and one related note.
Models are easy to test. They are often very simple objects without a lot of inherited context or functionality, and implicitly create a namespace for any logic that belongs to them so that your code is organized more cleanly.
For example, let's imagine you have a view controller with an IBAction
like so:
- (IBAction)someQuirkyButtonPressed:(UIButton *)button
{
if(self.currentUsers.count == 0)
return;
for(ABCUser *user in self.currentUsers)
{
if(user.isAllowedToBeQuirky == NO)
{
return;
}
}
[self doSomethingQuirkyWithUsers:self.currentUsers];
}
Now, let's say we want to write a test to ensure that if any user isn't allowed to be quirky, we are sure that -doSomethingQuirkyWithUsers:
isn't called.
We can of course, test this method itself by creating a test for the view controller, creating fixture data for the users, creating a mock for the view controller, calling the method, and verifying that -doSomethingQuirkyWithUsers:
really wasn't ever called. But that's a lot of work. And we can get the same assurance in a much simpler manner.
Let's go ahead and move the logic of that for-loop to the ABCUser
class itself, as a class method.
+ (BOOL)canDoSomethingQuirkyWithUsers:(NSArray *)users
{
if(users.count == 0)
return NO;
for(ABCUser *user in self.currentUsers)
{
if(user.isAllowedToBeQuirky == NO)
{
return NO;
}
}
return YES;
}
and let's update our IBAction
method appropriately:
- (IBAction)someQuirkyButtonPressed:(UIButton *)button
{
if([ABCUser canDoSomethingQuirkyWithUsers:self.currentUsers] == NO)
return;
[self doSomethingQuirkyWithUsers:self.currentUsers];
}
Because of the way this method reads, and the simplicty of its implementation, we can achieve our assurance that -doSomethingQuirkyWithUsers:
will not be called by simply writing tests to verify that -canDoSomethingQuirkyWithUsers:
does what it says it does. We'll do this by writing a test for each possible outcome of its implementation. For -canDoSomethingQuirkyWithUsers:
that means testing each path that results in return
statement; because it is a pure function, meaning it takes a value & returns another value without performing any side effects, we can be confident that testing those code paths is enough to ensure our code behaves as it should.
- (void)testThatYouCanDoSomethingQuirkyWhenAllUsersAllowIt
{
ABCUser *user1 = [ABCUser user];
user1.allowedToBeQuirky = YES;
ABCUser *user2 = [ABCUser user];
user2.allowedToBeQuirky = YES;
ABCUser *user3 = [ABCUser user];
user3.allowedToBeQuirky = YES;
NSArray *users = @[ user1, user2, user3 ];
XCTAssertTrue([ABCUser canDoSomethingQuirkyWithUsers:users]);
}
- (void)testThatYouCannotDoSomethingQuirkyWhenAnyUserDoesNotAllowIt
{
ABCUser *user1 = [ABCUser user];
user1.allowedToBeQuirky = YES;
ABCUser *user2 = [ABCUser user];
user2.allowedToBeQuirky = YES;
ABCUser *user3 = [ABCUser user];
user3.allowedToBeQuirky = NO;
NSArray *users = @[ user1, user2, user3 ];
XCTAssertFalse([ABCUser canDoSomethingQuirkyWithUsers:users]);
}
- (void)testThatYouCannotDoSomethingQuirkyIfThereAreNoUsers
{
NSArray *users = @[];
XCTAssertFalse([ABCUser canDoSomethingQuirkyWithUsers:users]);
}
As a side note, moving any code out of the view controller means that it becomes far more reusable in the rest of your code base. And since the tests are owned by the model and not the view controller that is using them, the tests cover all the usages of that logic and not just the original view controller without having to write duplicate tests. This isn't something inherit to models, but abstraction in general, and it applies to our next strategy for testing business logic.
One of the "problems" (quotes because it's not really a problem, just an unfortunate reality) is that we have a problem naming things. This is true of all software, of course, but on iOS we can point quite a bit of blame to developers coming in without a ton of general experience in software development, meaning they take what they learn from Apple and related sources and hold that knowledge dearly, as if it's the only way.
I'm not demeaning anyone here; it's amazing to me that anyone can just pick up a book on iOS development and a relatively short period of time compared to other high paying jobs have a career. Indeed, I am a college drop out after all. I know that feel.
But alas, it is an issue we deal with and one of the culminating effects is that we've ruined the word "Controller". as it's become synonymous with UIViewController subclasses in an implicit MVC context. But! it doesn't need to be that way. Conceptually, a controller only needs to do operations on model data. That's all. In the Model-View-Controller pattern (MVC), the controller plays this part, plus a few other roles that are specific to interacting with a view.
But we just want to test business logic, so we don't want a view. That's too much information for our tests and ruins our isolation.
What we can do is refactor logic into a controller whose job is literally only to own that logic, to provide a way to test it in isolation. For example, let's say we are writing a UIViewController that is currently the delegate and dataSource for a UITableView
instance that lives inside its view somewhere.
We want to test that the data we return from our implementations of the UITableViewDataSource
protocol are correct, and we want to do so automatically. We want unit tests. We already know we don't want to write tests for the UIViewController subclass itself. Too much context. No isolation. You know the drill.
So we can just create a class who's purpose is to return the data we want for this particular view controller, and then test that. And if you're wondering, because we're not talking about view controllers here, we'll have this class be a vanilla subclass of NSObject
.
Let's take a look at what we had before we decided to write these tests.
// ABCAwesomeViewController.h
@interface ABCAwesomeViewController : UIViewController<UITableViewDelegate, UITableViewDataSource>
@property (nonatomic, copy) NSArray *models;
@end
// ABCAwesomeViewController.m
@implementation ABCAwesomeViewController
...
#pragma mark - UITableViewDataSource
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return self.models.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForIndexPath:(NSIndexPath *)indexPath
{
ABCModel *model = self.models[indexPath.row];
ABCModelCell *cell = ...dequeue cell...;
cell.model = model;
return cell;
}
@end
And now let's take a look at how we'll refactor it to enable us to write tests on our implementation of the UITableViewDataSource
protocol.
// ABCAwesomeViewController.h
@interface ABCAwesomeViewController : UIViewController<UITableViewDelegate, UITableViewDataSource>
@property (nonatomic, strong) ABCAwesomeModelsController *modelsController;
- (void)setModels:(NSArray *)models;
@end
// ABCAwesomeViewController.m
@implementation ABCAwesomeViewController
- (void)viewDidLoad
{
[super viewDidLoad];
self.modelsController = [[ABCAwesomeModelsController alloc] init];
}
- (void)setModels:(NSArray *)models
{
self.modelsController.models = models;
[self.tableView reloadData];
}
#pragma mark - UITableViewDataSource
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return [self.modelsController numberOfRows];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForIndexPath:(NSIndexPath *)indexPath
{
ABCModel *model = [self.modelsController modelForIndexPath:indexPath];
ABCModelCell *cell = ...dequeue cell...;
cell.model = model;
return cell;
}
@end
// ABCAwesomeModelsController.h
@interface ABCAwesomeModelsController : NSObject<
@property (nonatomic, copy) NSArray *models;
- (NSInteger)numberOfRows;
- (ABCModel *)modelForIndexPath:(NSIndexPath *)indexPath;
@end
@implementation ABCAwesomeModelsController
- (NSInteger)numberOfRows
{
return self.models.count;
}
- (ABCModel *)modelForIndexPath:(NSIndexPath *)indexPath
{
if(indexPath == nil)
return nil;
NSInteger index = indexPath.row;
if(index >= self.models.count)
return nil;
return self.models[index];
}
Now that we have our controller setup, we can write tests for it.
- (NSArra *)testModels
{
ABCModel *user1 = [ABCModel model];
ABCModel *user2 = [ABCModel model];
ABCModel *user3 = [ABCModel model];
return @[ user1, user2, user3 ];
}
- (void)testThatTheControllerReturnsTheCorrectNumberOfRows
{
NSArray *models = [self testModels];
ABCAwesomeModelsController *controller = [[ABCAwesomeModelsController alloc] init];
controller.models = models;
XCTAssertTrue([controller numberOfRows] == models.count);
controller.models = nil;
XCTAssertTrue([controller numberOfRows] == 0);
}
- (void)testThatTheControllerReturnsTheCorrectModelForAGivenIndexPath
{
NSArray *models = [self testModels];
ABCAwesomeModelsController *controller = [[ABCAwesomeModelsController alloc] init];
controller.models = models;
NSIndexPath *validIndexPath = [NSIndexPath indexForRow:1 inSection:0];
XCTAssertTrue([controller modelForIndexPath:validIndexPath] == models[1]);
XCTAssertNil([controller modelsForIndexPath:nil]);
NSIndexPath *invalidIndexPath = [NSIndexPath indexForRow:42 inSection:0];
XCTAssertNil([controller modelForIndexPath:invalidIndexPath]);
}
Now we're sure the data we're working with is correct. We're free to make changes to the underlying implementation without affecting unrelated aspects of the view controller's code as we'll know if we break any of the fundamental logic ensured by the tests. And the code operating on our data no longer cares, or even knows about, anything related to the view that will eventually render it. Separation of concerns. Isolation. I can dig it.
We're mostly finished within the scope of this article but I wanted to address something that I was sort of skirting around for most of the words above these ones. None of the code up there tested the view itself, which often contains some of its own inherit logic that can easily break and would be guarded by a suite of unit tests.
But views are hard to tests. People do it, but it's cumbersome. Some people even rasterize the views and test them against source data. I'm not into that.
Here's what I say:
- Write Automated UI Acceptance Tests *
What does that mean? Well, I'm suggesting that it's better to write tests that simulate user interaction while simultaneous asserting that 1) the correct views are presented when you expect them to be and 2) that the views contains the correct elements to be interacted with.
Is this a perfect solution? Absolutely not. But in 2015 with tons of devices and screen resolutions, you will spend more time writing & updating the tests then providing awesome features or bug fixes to your users. And that's just a generalization. I'm sure there are teams with dedicated test engineers that will disagree with that statement, but for the majority of small time developers, automated UI acceptance tests offer the best mix of velocity and protection from bugs.
For implementing them, I suggest KIF (version 3). It has the added benefit of making sure your app works better for users that require accessibility features because it relies on that data to perform the tests.
Apple also has an offering for this as of iOS 8 and Xcode 6 but it is not as mature and doesn't offer the same feature set as KIF, which has served me quite well.
If you'd like to hear more about testing with KIF, I'm happy to write another piece about it, though it's out of scope for this particular article.
That's all I've got for now. What follows is a list of libraries I find useful for testing on iOS and some contextual information for them.
When I begin writing tests for my iOS projects, I create three additional targets in addition to however many targets are involved with the actual application itself. The first target is for Unit Tests & the second target is for UI Acceptance Tests. The third is a testing host for the Unit Tests to avoid having to add unrelated files used for testing to our test target.
So, if I were creating an app called Awesome.app, I would have the following targets.
- Awesome
- Awesome Unit Tests
- Awesome Unit Tests Host
- Awesome UI Tests
Now, if you're using CocoaPods you'll probably run into issues with their default target setup, so I suggest the following configuration. Note that a Podfile is a Ruby DSL (Domain Specific Language) so you can write Ruby code in your Podfile to avoid duplicating things.
platform :ios, '7.1'
source 'https://github.com/CocoaPods/Specs.git'
#
# Pods shared between the application and test targets.
#
def shared_pods
pod 'Mantle', '~> 1.5.3'
pod 'BlocksKit', '~> 2.2.5'
pod 'AFNetworking', '~> 2.5.0'
end
#
# Pods used by the actual application
#
def app_pods
shared_pods # Include the pods we're using in both testing and app targets
pod 'HockeySDK', '~> 3.6.2'
pod 'ZeroPush', '~> 2.0'
pod 'google-plus-ios-sdk', '~> 1.7.1'
pod 'Facebook-iOS-SDK', '~> 3.23.0'
pod 'SSPullToRefresh', '~> 1.2.3'
end
#
# Pods used only for testing
#
def testing_pods
shared_pods
pod 'JPSimulatorHacks', '~> 1.2.0'
pod 'OCMock', '~> 3.1.2'
end
#
# Now, we specify the targets for each individual
# target to tell Cocoapods not to assume anything
# that could potentially cause issues.
#
# Our actual app
target 'Awesome', :exclusive => true do
app_pods
end
# The test host for the Unit Tests should use the same pods as the real app
target 'Awesome Unit Tests Host', :exclusive => true do
app_pods
end
# Unit Tests gets the shared pods, testing pods, and some specific ones.
target 'Awesome Unit Tests', :exclusive => true do
testing_pods
pod 'Specta', :git => 'https://github.com/specta/specta.git', :tag => 'v0.3.0.beta1'
pod 'Expecta', '~> 0.3.1'
end
# Same deal for UI tests
target 'Awesome UI Tests', :exclusive => true do
testing_pods
pod 'KIF', '~> 3.1.1'
end
Perfecto, a sane CocoaPods setup for testing your app.
OCMock is a object mocking library. Mock objects are objects that pretend to be other objects, while tracking their own usage in your code and allow your to specifcy their behavior in certain situations. ie. You can make fake objects that pretend to be real, and when the code you're testing attempts to use them, they will return values you said they should for methods that your testing code will call on them. To make this easier, you need to get into the habit of using dependency injection. Dependency Injection is fancy way of saying that your code should be given its dependencies rather than getting them itself. The canonical example on iOS is NSUserDefaults
.
Say you have some intensely contrived code that depends on the user defaults store, and you want to test it.
- (BOOL)shouldReturnNoIfSomeUserDefaultsFlagIsYes
{
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
return ![usersDefaults boolForKey:@"someFlag"];
}
If we were test this, our tests need to make implicit assumptions about NSUserDefaults
and how it behaves, which is irrelevant to the actual logic we want to test. We can clean this up and make it more testable as so:
- (BOOL)shouldReturnNoIfSomeFlagIsYesInUserDefaults:(NSUserDefaults *)userDefaults
{
return ![usersDefaults boolForKey:@"someFlag"];
}
Your original code can be updated to use a real NSUserDefaults
instance:
[self shouldReturnNoIfSomeFlagIsYesInUserDefaults:[NSUserDefaults standardUserDefaults]];
And you can write testing code that no longer needs to care about how NSUserDefaults
actually works:
- (void)testThatTheObjectReturnsNoWhenItShould
{
id userDefaults = OCMClassMock([NSUserDefaults class]);
OCMStub([userDefaults stringForKey:@"someFlag"]).andReturn(@YES);
ABCSomeObject *object = [ABCSomeObject object];
XCTAssertFalse([object shouldReturnNoIfSomeFlagIsYesInUserDefaults:userDefaults]);
}
In this example, we used OCMClassMock
, which mocks an entire class by creating an opaque object that only behaves as you specify.
Sometimes, you'll need to use real objects that behave as their underlying implementation specifies, except for in certain scenarios that make your tests more convenient to write. In those cases, you can use a OCMPartialMock
to create a mock of an exiting object. It will still work with OCMStub
but will forward all other methods to the original object.
Mock objects also offer the ability to assert that certain method you wanted to be called on them are indeed called. With OCMock this is called verifying, and is covered by their documentation here. I don't find myself using this functionality often so I won't cover it here. OCMock has great documentation though so I encourage you to peruse it.
If this was particularly useful information, please let me if you'd like to hear more about mocking objects for testing purposes.
Xcode uses XCUnit, Apple's homemade testing framework, for writing tests. It works okay, I guess. I used XCUnit formatting in this article for familiarity's sake, but I personally prefer using Specta, a testing framework, and Expecta, a result matching framework, for writing my unit tests. They allow for more expressive testing code that's easier to read (to me personally) and makes it more clear what your tests are supposed to do. Using, Specta & Expecta, your tests would look like this:
SpecBegin(CHAContactsManager)
describe(@"Updating Contacts", ^{
__block ABCAwesomeThing *thing = nil;
beforeEach(^{
thing = [ABCAwesomeThing thing];
});
it(@"should do something awesome", ^{
expect([thing doSomethingAwesome]).to.beTruthy();
});
it(@"shouldn't run out of awesome", ^{
expect([thing doSomethingAwesome]).toNot.raise("ABCAwesomeThingRanOutOfAwesomeException");
});
afterEach(^{
contactsManager = nil;
});
});
SpecEnd
Specta is the part that allows you to use SpecBegin/SpecEnd
, beforeEach
, it
, etc.
Expecta is the bit that allows you to say cool things like expect(something).to.equal(soemthingElse);
and the like.
They're great libraries, I encourage you give them a shot.
The iOS simulator can sometimes be pretty frustrating while testing things, because often times you'll need to write code that assumes that your app has permission to access certain system content, such as photos, contacts, etc. By default, the simulator does not have access those things without prompting for user access just like in a live application. This is fine for normal development, but not so fine for automated testing.
JPSimulatorHacks uses hacks around these issues by modifying the simulator's underlying syqlite database that holds these permissions. You shouldn't attempt this in a real app (it wouldn't work anyway) but it's fine for the simulator and making your tests work.
You must really want to test your app! I think that's great. Hopefully you found the information above to be helpful in that endeavor.
At JayMobile, testing is important and is typically the very first step involved in new client projects. If that sounds like the sort of development shop you'd like to work with then I encourage you to reach out and chat with us about what you're building. You can reach as at [email protected].
We are deeply passionate about engineering as you can see by our heavy investment in testing on iOS. At the same time we understand the needs and priorities of fast moving, small companies and we know how to balance both sides of the hack-it-til-you-make it spectrum. As a principle, we never want to leave you with a codebase we aren't proud of hopefully you see now just how serious we are about that.
We look forward to hearing from you!