Pragmatism in the real world

Unit testing with Swift PM

As I tend to work with Swift on Linux, I’ve been working out how to unit test using the Swift Package manager. This article is my way of remembering what I’ve learnt so far! Let’s do this in the context of writing an a Todo entity from the Todo-Backend project.

Create a package

Creating a package is easy with Swift PM:

mkdir Todo
cd Todo
swift build --init library

(you can replace library with executable when creating an app)

When you run this, Swift PM will create the following:

.
├── Package.swift
├── Sources
│   └── Todo.swift
└── Tests
    ├── LinuxMain.swift
    └── Todo
        └── Todo.swift

Your Swift source files go in Sources and your unit tests go in Tests.

A simple Todo object

Inspecting the Todo-Backend test suite, we can see that a todo item has the following properties:

  • title: a string
  • completed: a boolean
  • order: an integer

Let’s build that in Swift and we’ll put it in Sources/Todo.swift:

public struct Todo {
    let title: String
    let completed: Bool
    let order: Int
    
    init(title: String, completed: Bool, order: Int) {
        self.title = title
        self.completed = completed
        self.order = order
    }
}

I’ve chosen to implement this as a value object so that title, completed and order cannot be changed after initialisation. If we need to change them, we’ll create a new object from the information in this one.

Setup the unit test code

SwiftPM automatically creates two files for us in the Tests directory: LinuxMain.swift which is a list of all the unit test classes in our package and Todo/Todo.swift which is our test class. Interestingly, the test class extends our main class, which is a fairly common pattern in Swift.

Swift and Swift Package Manager are both under heavy development and not always in sync with each other. With the 12th April 2016 build of Swift 3.0, the skeleton files created by Swift PM for running the tests under Linux are wrong! Let’s rewrite them.

Firstly, I prefer my unit test class to be separate, so delete Tests/Todo/Todo.swift and create Tests/Todo/TodoTests.swift. This file holds our unit test class, and we’ll write a simple unit test to get us started:

import XCTest
@testable import Todo

class TodoTests: XCTestCase {
	func testInitSetsTitle() {
        let todo = Todo(title: "test", completed: false, order: 1)
        
        XCTAssertEqual(todo.title, "test", "Incorrect title")
	}
}

Our unit test method is testInitSetsTitle and it simply instantiates a Todo and then uses XCTAssertEqual to check that the title of the object matches the string that we set.

Linux specific additions

To test under Linux, we need to do two things. Firstly we need to provide a list all the unit test methods in the test class as a static variable and secondly we need to list our test classes in a file called LinuxMain.swift.

To do the first bit, we add an extension to TodoTests within Tests/Todo/TodoTests.swift:

#if os(Linux)
extension TodoTests {
	static var allTests : [(String, TodoTests -> () throws -> Void)] {
		return [
			("testInitSetsTitle", testInitSetsTitle),
		]
	}
}
#endif 

This block extends the TodoTests class solely for Linux to add a static variable called allTests which is a an array of tuples consisting of the name of the test and the test’s method name. By convention, the string name is the same as the actual function name as it seems to be used by the test runner as a label.

Secondly we create Tests/LinuxMain.swift. This file was created by Swift PM, but as I said earlier is wrong, so needs to be replaced with:

import XCTest
@testable import TodoTestSuite

XCTMain([
    testCase(TodoTests.allTests),
])

This file simply instantiates XCTMain with a list of testCases for each test class’ allTests variable. This is another thing that really should be sorted via a build system by the time Swift 3 goes stable… As it is today though, it’s not hard to keep it updated.

Running the unit tests

To run the unit tests, we simply build the project with swift guid and then run swift test:

$ swift build
Compiling Swift Module 'Todo' (1 sources)

$ swift test
Compiling Swift Module 'TodoTestSuite' (1 sources)
Linking .build/debug/test-Package
Test Case 'Todo.testExample' started.
Test Case 'Todo.testExample' passed (0.0 seconds).
Executed 1 test, with 0 failures (0 unexpected) in 0.0 (0.005) seconds
Total executed 1 test, with 0 failures (0 unexpected) in 0.0 (0.005) seconds

That’s it

That’s all there is to unit testing in Swift under Linux. There’s not a lot of documentation yet, but I found Apple’s Writing Test with Swift documentation handy.

Other useful reading is SE-0019: Swift Testing is also worth reading as that documents how testing is implemented in Swift PM and the Additional Considerations for Swift on Linux document in the swift-corelibs-xctest repository.

7 thoughts on “Unit testing with Swift PM

  1. Hello, the swift build failed

    error: unable to invoke subcommand: /Library/Developer/CommandLineTools/usr/bin/swift-build (No such file or directory)

    Do you have an idea why?

    1. Mikos,

      I suspect that you're not using the latest 3.0 development snapshot from swift.org.

  2. Hi, I wonder if it is really possible to (XC)Test the 'executable' type of project in Swift 3.0 with the given approach on Linux… So, the main.swift is a problem.

  3. Hi Rob,

    if you do not mind – 2 more questions:

    1. @testable import TodoTestSuite – what is TodoTestSuite here? Do not see anything with that name…
    2. Why do we need these directives: #if os(Linux)
    I had some tests running on Linux without it, so wonder what's the exact need to have it.

    Thank you!

    1. 1. Can't remember :) Sorry!
      2. On macOS, XCTest determines all the tests to run via reflection. This doesn't work on Linux, so my understanding is that you need to list all the test classes so that swift test works.

Comments are closed.