Unit Testing
Unit Testing is an emotional topic for developers. It inspires a sense of superiority to its most zealous adherents, and evokes a feeling of inadequacy to non-practitioners. Cargo Cults like TDD stake their reputation on unit testing to the point of co-opting and conflating utility with morality.
It’s as close to a religious matter as programmers get, aside from the tabs-versus-spaces debate.
Objective-C developers have, for the most part, remained relatively apathetic to Unit Testing (“There’s that SenTest thing, but who uses that, really?”). Between static typing, typically manageable project sizes, and a compiler advanced enough to rewrite code for you, unit testing isn’t as much of a necessity as it is for more dynamic languages like Ruby (at least in practice).
But that’s not to say that Objective-C developers wouldn’t benefit from unit testing. In fact, as Objective-C continues to become more collaborative, with growing participation in the open source community, automated testing will become a necessity.
This week NSHipster will explore the world of unit testing frameworks, and how to set up an automated build system with Travis CI.
Unit Testing is a tool, just like any other tool. Its purpose is to make us better at our jobs, which is to produce robust, maintainable software.
It’s a simple enough premise: write code to construct environments that exercise the particular behavior of a given method, function, class, or feature. Variables are isolated in a scientific manner, so as to test assumptions with logical atomicity.
OCUnit
OCUnit, a.k.a. SenTestingKit, was integrated into Xcode 2.1 circa WWDC 2005, as a result of its use in the development of Core Data 1.0. Developed by Sen:te, OCUnit is actually one of the first unit testing libraries written for any language.
Unit Tests were added into a separate testing target in the Xcode Project. Each test file defines an Sen
subclass, which implements a series of methods beginning with the word test
. C assert
-style macros are used to fail tests if the specified condition is not met. Each test is run in sequence, independently of one another, with the results logged afterwards:
#import <Sen Testing Kit/Sen Testing Kit.h>
#import "Person.h"
@interface Test Person : Sen Test Case
@end
@implementation Test Person
- (void)test Full Name {
Person *person = [[Person alloc] init];
person.first Name = @"Pablo";
person.last Name = @"Picasso";
STAssert Equal Objects([person full Name], @"Pablo Picasso", nil);
}
The SenTestingKit assertions are about what you’d expect, offering bread-and-butter equality, existence, and truth checks:
STAssert
Nil() STAssert
Not Nil() STAssert
True() STAssert
False() STAssert
Equal Objects() STAssert
Equals() STAssert
Equals With Accuracy() STAssert
Throws() STAssert
Throws Specific() STAssert
Throws Specific Named() STAssert
No Throw() STAssert
No Throw Specific() STAssert
No Throw Specific Named() STAssert
True No Throw() STAssert
False No Throw() STFail()
And yet, as useful as tests are, they necessarily introduce friction into a development cycle. When project pressures begin to weigh, tests are the first thing to be thrown overboard. At some point, the tests stop passing (“we can worry about that later—now we have to ship!”)
The only chance testing has to remain relevant in high-pressure situations is to reduce that friction in development. Essentially, tests need to become both easier to write and easier to run.
Open Source Libraries
There are a myriad of open source libraries that attempt to make testing more palatable by way of syntactic sugar and features like method stubs, mock objects, and promises.
Here’s a list of some of the most useful open source libraries for unit testing:
Mock Objects | |
---|---|
OCMock | Erik Doernenburg |
OCMockito | Jon Reid | Matchers |
Expecta | Peter Jihoon Kim |
OCHamcrest | Jon Reid | BDD / TDD |
Specta | Peter Jihoon Kim |
Kiwi | Allen Ding |
Cedar | Pivotal Labs | Frameworks |
GHUnit | Gabriel Handford |
Automated Testing
Making tests easier to write is one thing, but getting them to run without affecting productivity is quite another.
Jenkins
For a long time, installing Jenkins on a dedicated Mac Mini was the state-of-the-art for automated build servers.
Aside from the fact that it’s kinda the worst thing ever to set-up, you can do a lot of cool things like notifying build status over IM or IRC, automatically distributing builds to TestFlight or HockeyApp with Shenzhen, and generating documentation with AppleDoc.
Travis
Until recently, automated unit testing for Objective-C was the privilege of projects that could dedicate the time and money to setup a CI server. Travis CI made CI available to the masses.
CI for Objective-C is more difficult than for other languages, because it needs to be done on a Mac. For economic reasons, there just isn’t a market for cloud-based OS X environments like there is for Linux. Fortunately, SauceLabs has built such a virtualized Mac cloud, and is graciously donating some of it to run tests for open source Objective-C projects on Travis-CI.
For an example of automated Objective-C unit testing in the wild, check out how AFNetworking does it.
The Tests
subdirectory contains separate projects for iOS and OS X targets, as well as a Podfile, which specifies all of the testing library dependencies. AFNetworking executes a Rake task, which shells out to xctool
.
All of the configuration for setup is defined in .travis.yml
:
.travis.yml
language: objective-c
before_install:
- brew update
- brew install xctool --HEAD
- cd Tests && pod install && cd $TRAVIS_BUILD_DIR
- mkdir -p "Tests/AFNetworking Tests.xcodeproj/xcshareddata/xcschemes" && cp Tests/Schemes/*.xcscheme "Tests/AFNetworking Tests.xcodeproj/xcshareddata/xcschemes/"
script: rake test
Full documentation for the Travis configuration file can be found on Travis-CI.org.
Once again, the direction of Objective-C has been directly influenced by the Ruby community. Those guys and gals are serious about testing. It’s not like we should complain, though: between CocoaPods, RubyMotion, and Nomad, Ruby has made Objective-C development better by several orders of magnitude.
The bottom line is that testing has come to Objective-C. It’s not always necessary, and it’s certainly not a silver bullet for writing great software, but it’s proven itself invaluable (especially for open source development). So give it a try now, before not testing becomes seriously uncool.