Avery Pierce
Posted on March 13, 2019
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")
}
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)
}
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.
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)
}
And here's the result:
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)
}
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)
}
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:))
}
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"])
}
Posted on March 13, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.