loading...
Clue

Writing better unit tests in Swift: Part One

clue_staff profile image Clue Originally published at dev.to on ・5 min read

Generating test data with factory methods

The problem

Look, we’re amongst friends here, so I feel I can be totally honest: I’ve written some really awful unit tests in my career. Twenty-line monsters with multiple mocks and asserts and asynchronous expectations. The sort of thing you see in books with titles like “How to Fix the Mess Left by the Idiot Who Worked Here Before.” I’ve also had to maintain that code after I’d written said unit tests and the less said about that the better. So suffice to say I make it a priority now to write “good” unit tests.

Before we get started, I’ll define what I think of as a “good” unit test. If we can agree that a unit test (or, really, any test) is composed of some setup, the action that we’re testing, and then an assertion about the effect of that action, then I would say that, put simply, a “good” unit test is one which makes each of those three components clear.

(Aside: this may differ from your definition of a “good” unit test, and that’s just something we’ll both have to live with.)

Over the course of a few blog posts I’m going to show you some of the things we do at Clue to ensure we’re always trying to write “good” unit tests. In this post we’re going to look at a simple trick we can use to minimise the setup part of our unit tests, while maintaining clarity.

Let’s say we want to unit test the equality method on a simple Swift struct.

struct User: Equatable {
 let name: String
 let email: String
 let needsVerification: Bool

static func ==(lhs: User, rhs: User) -\> Bool {
 return lhs.name == rhs.name
 && lhs.email == rhs.email
 && lhs.needsVerification == rhs.needsVerification
 }
}

There are (countably) infinitely many different argument combinations we could pass into this method for testing. Obviously we can’t test with all of them, so we have to pick a few cases that represent general trends in results. A valid way of unit testing this method (and, indeed, the way I would normally write such a method, using test-driven development) would be to start with the case where all properties of the two Users are equal, and then to test what happens when each individual property differs. This gives us a suite of tests like this:

func test\_equals\_allPropertiesMatch\_isTrue() {
 let sut = User(name: "", email: "", needsVerification: false)
 let other = User(name: "", email: "", needsVerification: false)
 XCTAssertEqual(sut, other)
 XCTAssertEqual(other, sut)
}

func test\_equals\_nameDiffers\_isFalse() {
 let sut = User(name: "Jo", email: "", needsVerification: false)
 let other = User(name: "", email: "", needsVerification: false)
 XCTAssertNotEqual(sut, other)
 XCTAssertNotEqual(other, sut)
}

func test\_equals\_emailDiffers\_isFalse() {
 let sut = User(name: "", email: "A", needsVerification: false)
 let other = User(name: "", email: "", needsVerifiation: false)
 XCTAssertNotEqual(sut, other)
 XCTAssertNotEqual(other, sut)
}

func test\_equals\_needsVerificationDiffers\_isFalse() {
 let sut = User(name: "", email: "", needsVerification: true)
 let other = User(name: "", email: "", needsVerification: false)
 XCTAssertNotEqual(sut, other)
 XCTAssertNotEqual(other, sut)
}

(sut stands for “subject under test” and for some reason it’s the only acronym variable name I’m okay with.)

These aren’t bad unit tests by any stretch of the imagination — in fact they’re basically fine — but they’re also not “good.” Why not?

Let’s look at the setup of our two Users. Each time we create them we have to pass in all of the properties that the init method expects. This is true even if, in the context of the test, we don’t actually care what they are. In the first test, for example, all we care about is that the properties Match — their values could be the names of the Spice Girls in ascending height order, or my favourite Berlin coffee shops (Bonanza & Five Elephant btw), and it would make no difference to the test so long as they were the same. Similarly in the other three tests we only care that one specific property Differs.

Having information in these tests that we don’t care about creates noise, and makes the tests harder to understand. That’s maybe okay for these small examples, but when you’re trying to work out why a more complicated unit test is failing an hour before you cut a release, you’ll be very thankful you cut out as much unnecessary noise as possible.

Wouldn’t it be just super great and awesome and peachy and other superlatives if our tests only contained pertinent information? And, of course, if that wasn’t possible all of this set up would have been for nothing.

The solution

So here’s what we’re going to do: we’re going to extend the User type in our test target in order to add a method that lets us configure a User with the data we care about, while using sensible defaults for the other properties. I tend to call this method create because I think it reads nicely. Whatever you call it, the method should look something like this:

extension User {
 static func create(
 name: String = "",
 email: String = "",
 needsVerification: Bool = false
 ) -\> User {
 return User(
 name: name,
 email: email,
 needsVerification: needsVerification
 )
 }
}

(Don’t worry, I don’t like the way you space out your code either.)

These create methods are really simple — they take in the same arguments as the type’s init method but give each one a default value. As a rule of thumb I’d tend to use "" for String arguments, 0 for numeric ones, false for Bools, etc. Whatever you decide to use, the really important thing is to try and keep it consistent across different create implementations that you write.

(Cool aside: once a few of your types have create methods you can start using the result of calling create without any arguments as default values too. So if you had Account(user: User) then in Account.create you could set the default value for the user argument to be User.create().)

Now, combined with Swift’s type inference abilities we can rewrite the above tests like so:

var sut, other: User!

func test\_equals\_allPropertiesMatch\_isTrue() {
 (sut, other) = (.create(), .create())
 XCTAssertEqual(sut, other)
 XCTAssertEqual(other, sut)
}

func test\_equals\_nameDiffers\_isFalse() {
 (sut, other) = (.create(name: "Jo"), .create())
 XCTAssertNotEqual(sut, other)
 XCTAssertNotEqual(other, sut)
}

func test\_equals\_emailDiffers\_isFalse() {
 (sut, other) = (.create(email: "A"), .create())
 XCTAssertNotEqual(sut, other)
 XCTAssertNotEqual(other, sut)
}

func test\_equals\_needsVerificationDiffers\_isFalse() {
 (sut, other) = (.create(needsVerification: true), .create())
 XCTAssertNotEqual(sut, other)
 XCTAssertNotEqual(other, sut)
}

Pretty nice, right? Now each test only contains the information that’s actually relevant to the specific test case. This is a Good Thing.

Anyway, that’s a brief introduction to writing nice factory methods in Swift. This is obviously just one small way you can improve your unit tests, but you’ve got to start somewhere right? Go forth now, and write unit tests. ✅

(Thanks to Josh Heald. We were pairing when this pattern for writing factory methods initially came together.)


Discussion

pic
Editor guide