Testing in Swift
Posted on Sun 10 June 2018 in Swift
We want to be able to write tests in Swift, and to do this we will
use the XCTest
framework.
We'll start by writing a function to test, something we haven't actually done yet.
(All the code here is available from [Github](
// add-function.swift [Swift 4.1, Xcode 9.3.1, 2018-06-10]
//
// Trivial demonstration of function syntax
func add(a: Int, b: Int) -> Int {
return a + b
}
func saybye() {
print("Bye!")
}
print("2 + 3 = \(add(a: 2, b: 3))")
print("2 + -2 = \(add(a: -2, b: 2))")
saybye()
There's not a huge amount to say about this, but in case it isn't clear:
- The keyword
func
introduces a function definition. - Arguments for the function are declared as
name:type
. - The return type for the function is declared after the
->
symbol. - If there is no value to be returned, the
->
can be omitted, as in the second function, which also takes no arguments. - The function body is enclosed in braces.
- We're using the
\(expression)
syntax to embed a calculated value in a string (with automatic type co-ercion). - When passing arguments to the function, we have to name them, but (despite this) they have to be passed in the declared order (you're not writing Python yet!)1
If we run this, the output is as expected:
$ ./add-function
2 + 3 = 5
2 + -2 = 0
Bye!
Naming the arguments when calling the function here seems to serve
no useful purpose: there's nothing special about a
and b
, and
requiring them to be named merely makes it harder to remember how
to invoke the function. Swift allows us to declare the arguments
with an _
prefix (then a space), and if we do so, the arguments
do not need to be (and indeed, are not allowed to be) named when
calling the function.
So this code is functionally equivalent:
// add-function-no-labels.swift
//
// Trivial demonstration of function syntax with anonymous arguments
func add(_ a: Int, _ b: Int) -> Int {
return a + b
}
func saybye() {
print("Bye!")
}
print("2 + 3 = \(add(2, 3))")
print("2 + -2 = \(add(-2, 2))")
saybye()
However, the point of this post wasn't really functions, but rather testing. And for that, we need to learn a little about packaging in Swift.
XCTest
, as the name implies, comes from Xcode, and expects your
Swift sources, tests, and outputs all to have a very specific
structure. This carries over to working with Swift from the command
line. By far the easiest way to proceed (outside Xcode) is to use the
swift package
command Apple supplies with the Xcode command-line
tools. To do this, we first need to create a directory to create our
package, then use the swift package
command to initialize the
structure of that package, like this:
$ mkdir adder
$ cd adder
$ swift package init --type library
Creating library package: adder
Creating Package.swift
Creating README.md
Creating .gitignore
Creating Sources/
Creating Sources/adder/adder.swift
Creating Tests/
Creating Tests/LinuxMain.swift
Creating Tests/adderTests/
Creating Tests/adderTests/adderTests.swift
Creating Tests/adderTests/XCTestManifests.swift
$ ls -R
Package.swift README.md Sources Tests
./Sources:
adder
./Sources/adder:
adder.swift
./Tests:
LinuxMain.swift adderTests
./Tests/adderTests:
XCTestManifests.swift adderTests.swift
Points to note:
- The packager has created some helper code for Linux too.
- It's also created a some trivial code to test and a single test for it.
- We've specified that we're building a library here, but we could have said we want an executable instead.
We could run the auto-generated test for the auto-generated code its tests for its code by saying:
$ swift test
but, we'll replace the code and test before doing so.
For the code, we'll simply use the add
function from above:
// adder.swift [Swift 4.1, Xcode 9.3.1, 2018-06-10]
//
// Simple integer add function for testing
func add(a: Int, b: Int) -> Int {
return a + b
}
using this to replace the "Hello, World"
-ish that the packager
generated for us.
import XCTest
@testable import adder
class adderTests: XCTestCase {
func testAddSmallIntegers() {
XCTAssertEqual(adder.add(a: 2, b:3), 5)
}
func testAddInverses() {
XCTAssertEqual(adder.add(a: 2, b:-2), 0)
XCTAssertEqual(adder.add(a: 10000000, b:-10000000), 0)
}
}
Points to note:
- We need the
@testable
before the import of our module to make it available to the test framework.2 - We subclass
XCTestCase
class fromXCTest
to form a test class, (following the usual x-unit pattern) - We use the
XCAssertEqual
function, fromXCTest
for our assertions. - The generated test code made the test class
final
, which prevents it from being subclassed. I'm not sure what the benefit of that is, so I've omitted it, but it would do no harm - We need to prefix our
add
function with the namespace qualifieradder.
when calling it in our tests. I am currently confused as to why this is the case, as everything I've read seems to suggest we shouldn't need to. We don't seem to need to prefix functions fromFoundation
withFoundation.
when we call them, so either the way we've defined or imported the library here is suboptimal, or there's a name clash I'm unaware of, or there's something else I've misunderstood. We'll come back to this (and I'll update this post) when I've understood more about Swift packaging and namespaces. - The generated test code also include the lines:
static var allTests = [
("testExample", testExample),
]
It looks as if that is necessary only for Linux (see here), so again, I've omitted it here.
This is what happens if we run the tests:
$ swift test
Compile Swift Module 'adderTests' (2 sources)
Test Suite 'All tests' started at 2018-06-09 13:03:53.957
Test Suite 'adderPackageTests.xctest' started at 2018-06-09 13:03:53.957
Test Suite 'adderTests' started at 2018-06-09 13:03:53.957
Test Case '-[adderTests.adderTests testAddInverses]' started.
Test Case '-[adderTests.adderTests testAddInverses]' passed (0.110 seconds).
Test Case '-[adderTests.adderTests testAddSmallIntegers]' started.
Test Case '-[adderTests.adderTests testAddSmallIntegers]' passed (0.000 seconds).
Test Suite 'adderTests' passed at 2018-06-09 13:03:54.067.
Executed 2 tests, with 0 failures (0 unexpected) in 0.110 (0.111) seconds
Test Suite 'adderPackageTests.xctest' passed at 2018-06-09 13:03:54.068.
Executed 2 tests, with 0 failures (0 unexpected) in 0.110 (0.111) seconds
Test Suite 'All tests' passed at 2018-06-09 13:03:54.068.
Executed 2 tests, with 0 failures (0 unexpected) in 0.110 (0.111) seconds
Happily, both tests pass.
Before concluding, we'll just show what happens if we have create a failing test, for example by changing the first test to expect 0 instead of 5 for the addition of 2 and 3.
$ swift test
Compile Swift Module 'adderTests' (2 sources)
Linking ./.build/x86_64-apple-macosx10.10/debug/adderPackageTests.xctest/Contents/MacOS/adderPackageTests
Test Suite 'All tests' started at 2018-06-09 13:13:56.386
Test Suite 'adderPackageTests.xctest' started at 2018-06-09 13:13:56.386
Test Suite 'adderTests' started at 2018-06-09 13:13:56.386
Test Case '-[adderTests.adderTests testAddInverses]' started.
Test Case '-[adderTests.adderTests testAddInverses]' passed (0.114 seconds).
Test Case '-[adderTests.adderTests testAddSmallIntegers]' started.
/Users/njr/lang/swift/cli-swift-examples/adder/Tests/adderTests/adderTests.swift:6: error: -[adderTests.adderTests testAddSmallIntegers] : XCTAssertEqual failed: ("5") is not equal to ("0") -
Test Case '-[adderTests.adderTests testAddSmallIntegers]' failed (0.002 seconds).
Test Suite 'adderTests' failed at 2018-06-09 13:13:56.502.
Executed 2 tests, with 1 failure (0 unexpected) in 0.116 (0.116) seconds
Test Suite 'adderPackageTests.xctest' failed at 2018-06-09 13:13:56.502.
Executed 2 tests, with 1 failure (0 unexpected) in 0.116 (0.116) seconds
Test Suite 'All tests' failed at 2018-06-09 13:13:56.502.
Executed 2 tests, with 1 failure (0 unexpected) in 0.116 (0.117) seconds
$
The output is a little but busy, but we can see that the crucial information about the failure is as follows (in which I've broken the single-line error message to enhance readability):
/Users/njr/lang/swift/cli-swift-examples/adder/Tests/adderTests/adderTests.swift:6:
error: -[adderTests.adderTests testAddSmallIntegers] : XCTAssertEqual failed:
("5") is not equal to ("0")
I'm slightly surprised at the way the failure is reporting the failure—making it look as though the values are strings—but the problem is clear.
That's probably enough for one post.
-
Swift's approach to function and argument names is clearly strongly influence by Objective C, which was itself strongly indludenced by Smalltalk. In a previous post we saw an example of a method for percent-encoding a string, which we called as
path.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlPathAllowed)
. This style of naming functions as verbs and arguments as parts of sentences explaining the qualification that the provide to the verb is idomatic, and went even further in Objective-C, where the "function" (actaually a message) would actually be calledaddPercentEncoding:withAllowedCharacters
. ↩ -
I'm not sure, at this point, what
@testtable
actually does, and have been a bit surprised at the paucity of information on it. I suspect it's not needed in Xcode, where most people develop Swift. But I can confirm that the tests will not run without this. ↩