Clean up your Xcode unit tests

avery

Avery Pierce

Posted on March 13, 2019

Clean up your Xcode unit tests

Apple's XCTest framework for Xcode offers only a handful of assertion statements, and almost all of them are trivial variations of XCTAssert. If you find yourself calling XCTAssertEqual over and over again in your tests, consider writing your own assertion functions! You may find that your test code is cleaner as a result.

Consider the following example: I have an unsorted list of contacts, and I want to make sure they are sorted alphabetically by last name.

func testSortAlphabeticallyByLastName() {
    let contact1 = Contact(firstName: "John", lastName: "Appleseed")
    let contact2 = Contact(firstName: "Paul", lastName: "Bunyan")
    let contact3 = Contact(firstName: "Davy", lastName: "Crockett")
    let unsortedContacts = [contact2, contact3, contact1]

    let sortedContacts = ContactSorter().sort(unsortedContacts, by: .lastName)

    // Contact does not conform to Equatable, so we test each element separately
    XCTAssertEqual(sortedContacts[0].lastName, "Appleseed")
    XCTAssertEqual(sortedContacts[1].lastName, "Bunyan")
    XCTAssertEqual(sortedContacts[2].lastName, "Crockett")
}
Enter fullscreen mode Exit fullscreen mode

This can be cleaned up. I dislike the 3 separate calls to XCTAssertEqual, and I also dislike that we're comparing the last name instead of the whole contact. We could make Contact conform to Equatable, but for this exercise, let's try to do without.

First, I want to write an function that asserts a contact has a given name. I might write it like this.

func testSortAlphabeticallyByLastName() {
    let contact1 = Contact(firstName: "John", lastName: "Appleseed")
    let contact2 = Contact(firstName: "Paul", lastName: "Bunyan")
    let contact3 = Contact(firstName: "Davy", lastName: "Crockett")
    let unsortedContacts = [contact2, contact3, contact1]

    let sortedContacts = ContactSorter().sort(unsortedContacts, by: .lastName)

    assert(sortedContacts[0], hasName: "John Appleseed")
    assert(sortedContacts[1], hasName: "Paul Bunyan")
    assert(sortedContacts[2], hasName: "Davy Crockett")
}

private func assert(_ contact: Contact, hasName fullName: String) {
    let contactFullName = "\(contact.firstName ?? "") \(contact.lastName ?? "")"
    XCTAssertEqual(contactFullName, fullName)
}
Enter fullscreen mode Exit fullscreen mode

There's just one problem. If the tests fail, Xcode highlights the line for XCTAssertEqual inside my function, instead of the line where the function is called. This isn't very helpful.

XCTAssertEqual failed: (

Did you know that XCTAssert (and its derivatives) has arguments for file and line number? By default, they're populated by the caller using the #file and #line values. My assert(_:hasName:) function can capture these values and pass them into XCTAssertEqual to have Xcode highlight the correct offending line.

Here's my updated function:

private func assert(_ contact: Contact, hasName fullName: String, file: StaticString = #file, line: UInt = #line) {
    let contactFullName = "\(contact.firstName ?? "") \(contact.lastName ?? "")"
    XCTAssertEqual(contactFullName, fullName, file: file, line: line)
}
Enter fullscreen mode Exit fullscreen mode

And here's the result:

XCTAssertEqual failed: (

This is already much nicer. However, I would like to clean this up even more. Let's start by flattening those three separate assertions into one.

func testSortAlphabeticallyByLastName() {
    let contact1 = Contact(firstName: "John", lastName: "Appleseed")
    let contact2 = Contact(firstName: "Paul", lastName: "Bunyan")
    let contact3 = Contact(firstName: "Davy", lastName: "Crockett")
    let unsortedContacts = [contact2, contact3, contact1]

    let sortedContacts = ContactSorter().sort(unsortedContacts, by: .lastName)

    assert(sortedContacts, areNamed: [
        "John Appleseed", 
        "Paul Bunyan", 
        "Davy Crockett"])
}

private func assert(_ contacts: [Contact], areNamed names: [String], file: StaticString = #file, line: UInt = #line) {
    contacts.enumerated().forEach { (index, contact) in
        let name = names[index]
        assert(contact, hasName: name, file: file, line: line)
    }
}

private func assert(_ contact: Contact, hasName fullName: String, file: StaticString = #file, line: UInt = #line) {
    let contactFullName = "\(contact.firstName ?? "") \(contact.lastName ?? "")"
    XCTAssertEqual(contactFullName, fullName, file: file, line: line)
}
Enter fullscreen mode Exit fullscreen mode

Next, let's create a convenience function for contact initialization. I'm not testing the contact initializer here, so we should refactor that out of the test body.

func testSortAlphabeticallyByLastName() {
    let contact1 = makeContact(named: "John Appleseed")
    let contact2 = makeContact(named: "Paul Bunyan")
    let contact3 = makeContact(named: "Davy Crockett")
    let unsortedContacts = [contact2, contact3, contact1]

    let sortedContacts = ContactSorter().sort(unsortedContacts, by: .lastName)

    assert(sortedContacts, areNamed: [
        "John Appleseed", 
        "Paul Bunyan", 
        "Davy Crockett"])
}

private func makeContact(named name: String) -> Contact {
    let names = name.split(separator: " ")
    let firstName = String(names.first!)
    let lastName = String(names.last!)
    return Contact(firstName: firstName, lastName: lastName)
}
Enter fullscreen mode Exit fullscreen mode

Let's go one step further and inline the contact names into one function.

func testSortAlphabeticallyByLastName() {
    let unsortedContacts = makeContacts(named: [
        "Paul Bunyan",
        "Davy Crockett",
        "John Appleseed"])

    let sortedContacts = ContactSorter().sort(unsortedContacts, by: .lastName)

    assert(sortedContacts, areNamed: [
        "John Appleseed",
        "Paul Bunyan",
        "Davy Crockett"])
}

private func makeContacts(named names: [String]) -> [Contact] {
    return names.map(makeContact(named:))
}
Enter fullscreen mode Exit fullscreen mode

I don't think this could get much cleaner! One call to set up our data, one call to exercise our code, and one call to check our work. Now let's add a test for sorting by first name.

func testSortAlphabeticallyByFirstName() {
    let unsortedContacts = makeContacts(named: [
        "Paul Bunyan",
        "Davy Crockett",
        "John Appleseed"])

    let sortedContacts = ContactSorter().sort(unsortedContacts, by: .firstName)

    assert(sortedContacts, areNamed: [
        "Davy Crockett",
        "John Appleseed",
        "Paul Bunyan"])
}
Enter fullscreen mode Exit fullscreen mode
💖 💪 🙅 🚩
avery
Avery Pierce

Posted on March 13, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related