XCTestCase /XCTestExpectation / measureBlock()
Although iOS 8 and Swift has garnered the lion’s share of attention of the WWDC 2014 announcements, the additions and improvements to testing in Xcode 6 may end up making some of the most profound impact in the long-term.
This week, we’ll take a look at XCTest
, the testing framework built into Xcode, as well as the exciting new additions in Xcode 6: XCTest
and performance tests.
Most Xcode project templates now support testing out-of-the-box. For example, when a new iOS app is created in Xcode with ⇧⌘N
, the resulting project file will be configured with two top-level groups (in addition to the “Products” group): “AppName” & “AppNameTests”. The project’s auto-generated scheme enables the shortcut ⌘R
to build and run the executable target, and ⌘U
to build and run the test target.
Within the test target is a single file, named “AppNameTests”, which contains an example XCTest
class, complete with boilerplate set
& tear
methods, as well as an example functional and performance test cases.
XCTestCase
Xcode unit tests are contained within an XCTest
subclass. By convention, each XCTest
subclass encapsulates a particular set of concerns, such as a feature, use case, or flow of an application.
Dividing up tests logically across a manageable number of test cases makes a huge difference as codebases grow and evolve.
setUp & tearDown
set
is called before each test in an XCTest
is run, and when that test finishes running, tear
is called:
class Tests: XCTest Case {
override func set Up() {
super.set Up()
// Put setup code here. This method is called before the invocation of each test method in the class.
}
override func tear Down() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
super.tear Down()
}
}
@interface Tests : XCTest Case
@property NSCalendar *calendar;
@property NSLocale *locale;
@end
@implementation Tests
- (void)set Up {
[super set Up];
// Put setup code here. This method is called before the invocation of each test method in the class.
}
- (void)tear Down {
// Put teardown code here. This method is called after the invocation of each test method in the class.
[super tear Down];
}
@end
These methods are useful for creating objects common to all of the tests for a test case:
var calendar: NSCalendar?
var locale: NSLocale?
override func set Up() {
super.set Up()
calendar = NSCalendar(identifier: NSCalendar Identifier Gregorian)
locale = NSLocale(locale Identifier: "en_US")
}
- (void)set Up {
[super set Up];
self.calendar = [NSCalendar calendar With Identifier:NSCalendar Identifier Gregorian];
self.locale = [NSLocale locale With Locale Identifier:@"en_US"];
}
Since
XCTest
is not intended to be initialized directly from within a test case definition, shared properties initialized inCase set
are declared as optionalUp var
s in Swift. As such, it’s often much simpler to forgoset
and assign default values instead:Up
var calendar: NSCalendar = NSCalendar(identifier: NSGregorian Calendar)
var locale: NSLocale = NSLocale(locale Identifier: "en_US")
Functional Testing
Each method in a test case with a name that begins with “test” is recognized as a test, and will evaluate any assertions within that function to determine whether it passed or failed.
For example, the function test
will pass if 1 + 1
is equal to 2
:
func test One Plus One Equals Two() {
XCTAssert Equal(1 + 1, 2, "one plus one should equal two")
}
- (void)test One Plus One Equals Two {
XCTAssert Equal(1 + 1, 2, "one plus one should equal two");
}
All of the XCTest Assertions You Really Need To Know
XCTest
comes with a number of built-in assertions, but one could narrow them down to just a few essentials:
Fundamental Test
To be entirely reductionist, all of the XCTest
assertions come down to a single, base assertion:
XCTAssert(expression, format...)
XCTAssert(expression, format...);
If the expression evaluates to true
, the test passes. Otherwise, the test fails, printing the format
ted message.
Although a developer could get away with only using XCTAssert
, the following helper assertions provide some useful semantics to help clarify what exactly is being tested. When possible, use the most specific assertion available, falling back to XCTAssert
only in cases where it better expresses the intent.
Boolean Tests
For Bool
values, or simple boolean expressions, use XCTAssert
& XCTAssert
:
XCTAssert True(expression, format...)
XCTAssert False(expression, format...)
XCTAssert True(expression, format...);
XCTAssert False(expression, format...);
XCTAssert
is equivalent toXCTAssert
.True
Equality Tests
When testing whether two values are equal, use XCTAssert[Not]Equal
for scalar values and XCTAssert[Not]Equal
for objects:
XCTAssert Equal(expression1, expression2, format...)
XCTAssert Not Equal(expression1, expression2, format...)
XCTAssert Equal(expression1, expression2, format...);
XCTAssert Not Equal(expression1, expression2, format...);
XCTAssert Equal Objects(expression1, expression2, format...);
XCTAssert Not Equal Objects(expression1, expression2, format...);
XCTAssert[Not]Equal
is not necessary in Swift, since there is no distinction between scalars and objects.Objects
When specifically testing whether two Double
, Float
, or other floating-point values are equal, use XCTAssert[Not]Equal
, to account for any issues with floating point accuracy:
XCTAssert Equal With Accuracy(expression1, expression2, accuracy, format...)
XCTAssert Not Equal With Accuracy(expression1, expression2, accuracy, format...)
XCTAssert Equal With Accuracy(expression1, expression2, accuracy, format...);
XCTAssert Not Equal With Accuracy(expression1, expression2, accuracy, format...);
In addition to the aforementioned equality assertions, there are
XCTAssert
&Greater Than[Or Equal] XCTAssert
, which supplementLess Than[Or Equal] ==
with>
,>=
,<
, &<=
equivalents for comparable values.
Nil Tests
Use XCTAssert[Not]Nil
to assert the existence (or non-existence) of a given value:
XCTAssert Nil(expression, format...)
XCTAssert Not Nil(expression, format...)
XCTAssert Nil(expression, format...);
XCTAssert Not Nil(expression, format...);
Unconditional Failure
Finally, the XCTFail
assertion will always fail:
XCTFail(format...)
XCTFail(format...);
XCTFail
is most commonly used to denote a placeholder for a test that should be made to pass. It is also useful for handling error cases already accounted by other flow control structures, such as the else
clause of an if
statement testing for success.
Performance Testing
New in Xcode 6 is the ability to benchmark the performance of code:
func test Date Formatter Performance() {
let date Formatter = NSDate Formatter()
date Formatter.date Style = .Long Style
date Formatter.time Style = .Short Style
let date = NSDate()
measure Block() {
let string = date Formatter.string From Date(date)
}
}
- (void)test Date Formatter Performance {
NSDate Formatter *date Formatter = [[NSDate Formatter alloc] init];
date Formatter.date Style = NSDate Formatter Long Style;
date Formatter.time Style = NSDate Formatter Short Style;
NSDate *date = [NSDate date];
[self measure Block:^{
NSString *string = [date Formatter string From Date:date];
}];
}
The test output shows the average execution time for the measured block as well as individual run times and standard deviation:
Test Case '-[_Tests test Date Formatter Performance]' started.
<unknown>:0: Test Case '-[_Tests test Date Formatter Performance]' measured [Time, seconds] average: 0.000, relative standard deviation: 242.006%, values: [0.000441, 0.000014, 0.000011, 0.000010, 0.000010, 0.000010, 0.000010, 0.000010, 0.000010, 0.000010], performance Metric ID:com.apple.XCTPerformance Metric_Wall Clock Time, baseline Name: "", baseline Average: , max Percent Regression: 10.000%, max Percent Relative Standard Deviation: 10.000%, max Regression: 0.100, max Standard Deviation: 0.100
Test Case '-[_Tests test Date Formatter Performance]' passed (0.274 seconds).
Performance tests help establish a baseline of performance for hot code paths. Sprinkle them into your test cases to ensure that significant algorithms and procedures remain performant as time goes on.
XCTestExpectation
Perhaps the most exciting feature added in Xcode 6 is built-in support for asynchronous testing, with the XCTest
class. Now, tests can wait for a specified length of time for certain conditions to be satisfied, without resorting to complicated GCD incantations.
To make a test asynchronous, first create an expectation with expectation
:
let expectation = expectation With Description("...")
XCTest Expectation *expectation = [self expectation With Description:@"..."];
Then, at the bottom of the method, add the wait
method, specifying a timeout, and optionally a handler to execute when either the conditions of your test are met or the timeout is reached (a timeout is automatically treated as a failed test):
wait For Expectations With Timeout(10) { error in
…
}
[self wait For Expectations With Timeout:10 handler:^(NSError *error) {
…
}];
Now, the only remaining step is to fulfill
that expecation
in the relevant callback of the asynchronous method being tested:
expectation.fulfill()
[expectation fulfill];
Always call
fulfill()
at the end of the asynchronous callback—fulfilling the expectation earlier can set up a race condition where the run loop may exit before completing the test. If the test has more than one expectation, it will not pass unless each expectation executesfulfill()
within the timeout specified inwait
.For Expectations With Timeout()
Here’s an example of how the response of an asynchronous networking request can be tested with the new XCTest
APIs:
func test Asynchronous URLConnection() {
let URL = NSURL(string: "https://nshipster.com/")!
let expectation = expectation With Description("GET \(URL)")
let session = NSURLSession.shared Session()
let task = session.data Task With URL(URL) { data, response, error in
XCTAssert Not Nil(data, "data should not be nil")
XCTAssert Nil(error, "error should be nil")
if let HTTPResponse = response as? NSHTTPURLResponse,
response URL = HTTPResponse.URL,
MIMEType = HTTPResponse.MIMEType
{
XCTAssert Equal(response URL.absolute String, URL.absolute String, "HTTP response URL should be equal to original URL")
XCTAssert Equal(HTTPResponse.status Code, 200, "HTTP response status code should be 200")
XCTAssert Equal(MIMEType, "text/html", "HTTP response content type should be text/html")
} else {
XCTFail("Response was not NSHTTPURLResponse")
}
expectation.fulfill()
}
task.resume()
wait For Expectations With Timeout(task.original Request!.timeout Interval) { error in
if let error = error {
print("Error: \(error.localized Description)")
}
task.cancel()
}
}
- (void)test Asynchronous URLConnection {
NSURL *URL = [NSURL URLWith String:@"https://nshipster.com/"];
NSString *description = [NSString string With Format:@"GET %@", URL];
XCTest Expectation *expectation = [self expectation With Description:description];
NSURLSession *session = [NSURLSession shared Session];
NSURLSession Data Task *task = [session data Task With URL:URL
completion Handler:^(NSData *data, NSURLResponse *response, NSError *error)
{
XCTAssert Not Nil(data, "data should not be nil");
XCTAssert Nil(error, "error should be nil");
if ([response is Kind Of Class:[NSHTTPURLResponse class]]) {
NSHTTPURLResponse *http Response = (NSHTTPURLResponse *)response;
XCTAssert Equal(http Response.status Code, 200, @"HTTP response status code should be 200");
XCTAssert Equal Objects(http Response.URL.absolute String, URL.absolute String, @"HTTP response URL should be equal to original URL");
XCTAssert Equal Objects(http Response.MIMEType, @"text/html", @"HTTP response content type should be text/html");
} else {
XCTFail(@"Response was not NSHTTPURLResponse");
}
[expectation fulfill];
}];
[task resume];
[self wait For Expectations With Timeout:task.original Request.timeout Interval handler:^(NSError *error) {
if (error != nil) {
NSLog(@"Error: %@", error.localized Description);
}
[task cancel];
}];
}
Mocking in Swift
With first-class support for asynchronous testing, Xcode 6 seems to have fulfilled all of the needs of a modern test-driven developer. Well, perhaps save for one: mocking.
Mocking can be a useful technique for isolating and controlling behavior in systems that, for reasons of complexity, non-determinism, or performance constraints, do not usually lend themselves to testing. Examples include simulating specific networking interactions, intensive database queries, or inducing states that might emerge under a particular race condition.
There are a couple of open source libraries for creating mock objects and stubbing method calls, but these libraries largely rely on Objective-C runtime manipulation, something that is not currently possible with Swift.
However, this may not actually be necessary in Swift, due to its less-constrained syntax.
In Swift, classes can be declared within the definition of a function, allowing for mock objects to be extremely self-contained. Just declare a mock inner-class, override
and necessary methods:
func test Fetch Request With Mocked Managed Object Context() {
class Mock NSManaged Object Context: NSManaged Object Context {
override func execute Fetch Request(request: NSFetch Request!, error: Autoreleasing Unsafe Pointer<NSError?>) -> [Any Object]! {
return [["name": "Johnny Appleseed", "email": "[email protected]"]]
}
}
let mock Context = Mock NSManaged Object Context()
let fetch Request = NSFetch Request(entity Name: "User")
fetch Request.predicate = NSPredicate(format: "email ENDSWITH[cd] %@", "@apple.com")
fetch Request.result Type = .Dictionary Result Type
var error: NSError?
let results = mock Context.execute Fetch Request(fetch Request, error: &error)
XCTAssert Nil(error, "error should be nil")
XCTAssert Equal(results.count, 1, "fetch request should only return 1 result")
let result = results[0] as [String: String]
XCTAssert Equal(result["name"] as String, "Johnny Appleseed", "name should be Johnny Appleseed")
XCTAssert Equal(result["email"] as String, "[email protected]", "email should be [email protected]")
}
With Xcode 6, we’ve finally arrived: the built-in testing tools are now good enough to use on their own. That is to say, there are no particularly compelling reasons to use any additional abstractions in order to provide acceptable test coverage for the vast majority apps and libraries. Except in extreme cases that require extensive stubbing, mocking, or other exotic test constructs, XCTest assertions, expectations, and performance measurements should be sufficient.
But no matter how good the testing tools have become, they’re only good as how you actually use them.
If you’re new to testing on iOS or OS X, start by adding a few assertions to that automatically-generated test case file and hitting ⌘U
. You might be surprised at how easy and—dare I say—enjoyable you’ll find the whole experience.