Unit Testing in Swift
Swift, being all the rage these last four days, has definitely livened up our programming chat room quite a bit. With cautious optimism, we (Betsy and Brad) delved into the Xcode beta, curious about the state of testing with Swift. For the purposes of this article, we’ll only cover the basics of unit testing with XCTest.framework.
A Simple Test
Just to get things rolling, we wrote a Plain Old Swift Object to represent firewood.
class Firewood {
var charred: Bool
init() {
println("initializing our firewood")
charred = false
}
func burn() {
charred = true
}
}
As you can see, we have a simple initializer and one function to burn the firewood. Now, let’s write a simple test to assert that burning the firewood chars it.
import XCTest
class SimpleFirewoodTests: XCTestCase {
func testBurningActuallyChars() {
let firewood = Firewood()
firewood.burn()
assert(firewood.charred, "should be charred after burning")
}
}
Using a Common Subject
If we have multiple tests using the same setup for an object under test (or “subject” if we use Ruby’s RSpec parlance), we don’t want to have to repeat the initialization inside each test. That’s just not DRY. So, how do we do this?
After some trial and error involving opaque runtime errors, we learned the answer definitely does not involve overriding the init method for XCTestCase subclasses. Do not go there. Hic sunt dracones.
Instead, you’ll want to declare and initialize an instance variable with let (or var) as shown:
class SimpleDRYFirewoodTests: XCTestCase {
let firewood = Firewood()
func testBurningActuallyChars() {
firewood.burn()
assert(firewood.charred, "should be charred after burning")
}
}
This may seem a little counterintuitive, because you might ask “What happens to the firewood object in subsequent test blocks if I mutate it in one of them?” That takes us to the next section.
How XCTest Handles Multiple Tests in One TestCase
XCTest avoids using dirty objects from previous tests by instantiating a brand-new XCTestCase object for each test function. That is, if you have three tests in your XCTestCase subclass, the framework will instantiate three objects of the whole XCTestCase subclass. Not only that, but it does all of this work before the Test Suite even starts. Observe:
class FirewoodTests: XCTestCase {
let firewood = Firewood()
func testNewObjectUnderTestForEachTest1() {
println("testing that we have a new object in 1st test")
assert(!firewood.charred, "shouldn’t be charred before burning")
firewood.burn()
}
func testNewObjectUnderTestForEachTest2() {
println("testing that we have a new object in 2nd test")
assert(!firewood.charred, "shouldn’t be charred before burning")
firewood.burn()
}
func testBurningActuallyChars() {
firewood.burn()
assert(firewood.charred, "should be charred after burning")
}
}
Since our Subject prints out “initializing our firewood” each time init is called, we can see where that happens in relation to everything else in a test run. Here’s the beginning of the log output:
initializing our firewood
initializing our firewood
initializing our firewood
Test Suite 'All tests' started at 2014-06-06 16:49:07 +0000
Note how three Firewood objects are created, presumably by three different FirewoodTests objects before the Test Suite even begins its output.
Conclusion
We hope this article will help you avoid some of the missteps that we took. It seems unit testing in Swift is implemented well despite the dearth of documentation. Does anyone else have adventures to share about testing with Swift?