Search

A Rubyist’s Perspective on Testing in Swift

Josh Justice

7 min read

Oct 23, 2016

A Rubyist’s Perspective on Testing in Swift

One of the frustrating things about getting into testing is figuring out which tools to use. In the case of iOS development, Xcode offers built-in frameworks for unit and user interface testing, but there are a number of open-source testing tools as well. Which should you use?

That’s what I was hoping to find out recently when I took a week-long hacking vacation (“hackcation”?) to start an iOS app side project. I learned testing in Ruby, which is known for the maturity of its testing tools and practices, and I was interested to see how they applied in the context of Swift. After a week, here are some of my impressions.

Testing Workflow

I test-drove out all the basic functionality of the app using UI tests. When that was done, I switched to UI tweaks, things like styling views and adding animations. I didn’t try to test-drive those changes: as Graham Lee wrote in Test-Driven iOS Development, tests are better for requirements that have a clear yes/no answer. Instead, I just reran the test suite to make sure I didn’t break anything. This did mean that I occasionally got out of the habit of writing tests first, which bit me a few times.

Xcode supports running tests individually from the sidebar, but in my project I couldn’t get the tests to show up or run reliably. To work around this I manually disabled test classes, then enabled the ones I wanted to run at a given time and used the Test command as usual. This worked, but it was the most cumbersome part of the experience.

User Interface Tests

Xcode’s built-in XCUI testing didn’t work very reliably for me either: even simple tests like tapping a button sometimes failed when I ran them right after recording, and I wasn’t able to fix them. I tried the KIF framework as an alternative and got more satisfying results. KIF’s main examples are in Objective-C, but I was able to follow the instructions to get it running in Swift without any trouble.

I wrote my UI tests to assume I was starting on the main screen of the app. When some tests didn’t end on the main screen, this would cause subsequent tests to break. Working around this was easy at first, because the app had a persistent tab bar, so all I had to do was tap on the first tab bar item in the setUp() method of a base test class. Eventually, though, I removed the tab bar, so then I had to add test-specific code at the end of each test to get back to the main screen.

I started out trying to write my UI tests with Quick, a “spec”-style framework for Swift. Spec-style tests use plain English strings for test descriptions, which encourages simple, clear explanations of the purpose of each test:

class WidgetSpec: QuickSpec {
    override func spec() {
        describe("nextWidget") {
            it("returns the next widget in the sequence") {
                let widget = Widget(sequenceNumber: 27)
                let nextNumber = try! widget.nextWidget.sequenceNumber
                expect(nextNumber).to(equal(28))
            }

            context("when this is the last widget") {
                it("throws an error") {
                    let widget = Widget(sequenceNumber: 99)
                    expect{ try widget.nextWidget }.to(throwError(InvalidWidgetError))
                }
            }
        }
    }
}

When I tried Quick for UI tests, I ran into a problem using KIF’s tester() method. Since it() blocks are closures, Swift 3’s syntax requires adding an explicit self, making the call self.tester(). This was enough of a readability problem to convince me to switch back to XCTest for UI tests. But I did continue to use Quick’s Nimble expectations library so I could replace, for example, XCTAssertEquals(…) with expect(…).to(equal(…)).

Unit Tests

Unit tests didn’t present the same problems for Quick as my UI tests did, so I did use Quick for them. For a team project, though, I would have stuck to the built-in XCTest. Other iOS developers are much more likely to have prior experience with XCTest, and it’s easier to find information about it online.

As a Swift newbie, I wasn’t sure how to handle throwing functions that shouldn’t throw in a particular test. In XCTest I could add throws to my test method declarations:

func testCreatingAWidget() throws {
    let widget = try widgetStore.create(name: "Test Widget")
    // ...
}

But that didn’t work in Quick: the closures passed to it can’t be declared to throw. Adding do/catch constructs fixed the compilation errors but hindered readability:

it("creates a widget") {
    do {
        let widget = try widgetStore.create(name: "Test Widget")
    } catch {
        // ...
    }

    // ...
}

Instead, I preceded the calls to the throwable methods with try! instead of try:

it("creates a widget") {
    let widget = try! widgetStore.create(name: "Test Widget")
    // ...
}

If one of these functions threw an error it would have resulted in a runtime error. This isn’t great, but it’s good enough until I can see if I can override it() to allow throwable closures.

In unit tests I’m a big fan of mocks, a type of test double focused around testing the interactions between different objects. Unfortunately, automatically creating mocks is hard in Swift because of the lack of readwrite reflection. A few libraries provide some assistance, but I decided to start out hand-rolling my own test doubles, and that’s worked fine so far. Instead of rigidly following the most commonly-accepted definition of the term “mock,” I added features only as my tests needed them. For example, say I have a WidgetStore protocol:

protocol WidgetStore {
    func find(_ widgetID: Int) -> Widget?
}

I’d like a test double conforming to WidgetStore that makes it easy for me to set up test data. I’d like to be able to use it like this:

let testWidget = Widget(name: "My Awesome Widget")
widgetStoreDouble.canReceiveFind(withID: 42, andReturn: testWidget)

let resultWidget = widgetStoreDouble.find(42)
print(resultWidget.name) // => "My Awesome Widget"

Here’s an implementation of a WidgetStoreDouble that works this way:

class WidgetStoreDouble: WidgetStore {

    private var widgetsToFind: [Int:Widget] = [:]

    func canReceiveFind(withID widgetID: Int, andReturn widget: Widget) {
        widgetsToFind[widgetID] = widget
    }

    func find(_ widgetID: Int) -> Widget? {
        return widgetsToFind[widgetID]
    }

}

Working With Core Data Models

One way set up test data is to have your UI tests tap through the app to create it — but this can make tests slow and fragile. Luckily, since KIF runs in the same process as your app, there’s an alternative: you can use model classes to create data directly. I just had to make sure to set up the data before my view controllers asked for it, and to clear out the data after each test so they wouldn’t pollute each other’s data:

// clear out any pre-existing data
widgetStore.deleteAll()

// set up this test's data
widgetStore.createWidget(withName: "Test Widget")

// check for it in the UI
tester().tapView(withAccessibilityLabel: "All Widgets")
let widgetTable = tester().waitForView(withAccessibilityLabel: "Widget Table") as! UITableView
let firstCell = widgetTable.cellForRow(at: IndexPath(row: 0, section: 0))
expect(firstCell.textLabel?.text).to(equal("Test Widget"))

In UI tests I used store classes to instantiate my models, but I didn’t want that dependency in my unit tests. Instead, I directly called the Core Data method my store classes call: NSEntityDescription.insertNewObject(). (Since my app targets iOS 9 I couldn’t use NSManagedObject’s beautiful new init(context:) initializer, and I didn’t think to use the BNR Core Data Stack.) Since NSEntityDescription.insertNewObject() is so cumbersome, I created helper methods to wrap the instantiations – as many different helpers as I needed to make each line as clear as possible. For example, I had a createWidget(for: owner, completedAt: date) method, then I added the very-similar createCompletedWidget(for: owner) for when the exact completion date doesn’t matter.

To decide what to test in Core Data models, I applied the advice that the BNR backend team gave me for Rails models: don’t bother testing simple stored attributes, but do test calculated properties and custom logic. This advice worked great: those tests gave me a lot of confidence that I was handling edge cases.

In some cases I wanted test doubles for my Core Data models, but I ran into some naming trouble with these. Usually, I name my protocols the simplest name for the concept and add a qualifier to the concrete class — for example, a WidgetStore protocol and a CoreDataWidgetStore class conforming to it). The problem was that when Xcode generates NSManagedObject subclasses, it gives them unprefixed names (i.e. Widget). To name my protocols, I appended -Model to the name (i.e. WidgetModel), and that seemed clear enough.

Was It Worth It?

Was all this testing worth the time it took? Well, my tests have already allowed me to perform quite a few refactorings to keep my code clean, including changing out a UITableView for a UICollectionView, reorganizing view controller flows on the storyboard, improving the smoothness of UI animations, abstracting out duplicate code, moving code from view controllers into custom view subclasses, and renaming classes to better reflect what they’re used for. That’s a lot of improvements I wouldn’t have been able to make in such a short time without tests as a safety net.

So, overall, starting out a Swift project with TDD was a great experience. Whether you want to start with XCTest or Quick, Xcode UI testing or KIF, I highly recommend dipping your toes into iOS testing to figure out a toolset that works for you!

Josh Justice

Author Big Nerd Ranch

Josh Justice has worked as a developer since 2004 across backend, frontend, and native mobile platforms. Josh values creating maintainable systems via testing, refactoring, and evolutionary design, and mentoring others to do the same. He currently serves as the Web Platform Lead at Big Nerd Ranch.

Speak with a Nerd

Schedule a call today! Our team of Nerds are ready to help

Let's Talk

Related Posts

We are ready to discuss your needs.

Not applicable? Click here to schedule a call.

Stay in Touch WITH Big Nerd Ranch News