How can you do TDD with Swift?

If you haven’t done TDD in a compiled language like Swift before, you may be wondering:

How can you do TDD since your code won’t compile if the objects your test references don’t yet exist?

An interpreted language like Ruby or JavaScript may feel like a more natural fit for TDD than a compiled language like Swift since you can write tests for objects that don’t exist without experiencing any compiler errors.

So how can you do TDD in a compiled language?

You can just write the test code and let it fail to compile. If you treat “failure to compile” the same way you’d treat a failed test in an interpreted language, it’s pretty simple. A failure is a failure, whether it’s caught by the compiler or by one of your tests.

To demonstrate, we’ll do TDD on a Project class that we want to be able to turn into a dictionary so it can be serialized later.

1. Create a test and instantiate the class that you want to exist

Since we want to test creating a dictionary from a Project, we’ll need an instance of a Project (which doesn’t even exist yet).

class ProjectTests: XCTestCase {
    func test_asDictionary() {
        let project = Project(id: 5)
    }
}

This fails to compile, so treat it as a test failure. We’re off to a great start. Seriously. It’s TDD – we want our test to fail at first.

Test status: red.

2. Write the class that you want to exist

To fix the compiler error, we need a Project with an init that has an id parameter, so let’s create it:

class Project { 
    private let id: Int
    
    init(id: Int) {
        self.id = id
    }
}

This fixes the compiler error, so the test is passing again.

Test status: green.

3. In the test, call the method that you want to exist

Now we want to call the asDictionary method on our Project instance, which should give us the dictionary representation of the Project.

func test_asDictionary() {
    let project = Project(id: 5)
    let dict = project.asDictionary()
}

This fails to compile, so the test case is red again. Good – we can move on.

Test status: red.

4. Write the method that you want to exist

In the Project class, we can now implement the asDictionary method, but we need to be careful to only write the bare minimum to make the test pass. (In other words, DON’T TOUCH THE ID PROPERTY YET!)

func asDictionary() -> [String: AnyObject] {
    return [String: AnyObject]()
}

Remember that in TDD we’re always trying to do the simplest thing possible to make the test pass. So here, we’re just returning an empty dictionary – we don’t need to put any keys or values into it yet since we don’t have any failing tests that tell us to do so.

This makes the test green again since it fixes the compiler error. Of course, our test doesn’t tell us much yet, so we’ll need to write an assertion.

Test status: green.

5. In the test, write an assertion

Now we can make an assertion on the return value from the asDictionary method. We want our Project‘s id to appear in the dictionary. So our test becomes this:

func test_asDictionary() {
    let project = Project(id: 5)
    let dict = project.asDictionary()
        
    XCTAssertEqual(dict["id"] as? Int, 5)
}

This compiles, but when we run it, the test fails, telling us that nil isn’t equal to 5. Our test is failing again, but no problem – we can fix it!

Test status: red.

6. Implement the method to make the test pass

Now we can write the logic for the method that’ll fulfill the assertion to make the test pass again.

Back in our Project, we can update asDictionary:

func asDictionary() -> [String: AnyObject] {
    return ["id": 5]
}

What?, you may be thinking. Shouldn’t we be returning the id now instead of 5? If we’re truly practing TDD, then no, we shouldn’t be returning the value of the id property yet. Returning the hard-coded value 5 here is the simplest way to make our test pass. If we want to assert that the value of id is returned in the dictionary, we need another test.

Test status: green. Assertion status: not good enough.

7. Write another test with a new assertion

Now we can write a full test without any compiler errors. We’ll just create a new test that gives the Project an id other than 5, calls asDictionary, and makes the assertion.

func test_asDictionary_with_id_7() {
    let project = Project(id: 7)
    let dict = project.asDictionary()
        
    XCTAssertEqual(dict["id"] as? Int, 7)
}

This will fail since asDictionary is always returning 5 for the id. Which is great, because now we have some pretty good assertions about how the code should work.

Test status: red. Assertion status: good.

8. Implement the method to make the test pass

Now we can update asDictionary to make our test pass. But this time, returning a hard-coded ["id": 7] won’t work since that would break our first test. We can update the method to return the value of the id property in the dictionary, like so:

func asDictionary() -> [String: AnyObject] {
    return ["id": id]
}

And when we run the tests, they pass! Now we can be confident that asDictionary will always return the id in the dictionary.

Test status: green. Assertion status: good.

Conclusion

You can practice TDD in compiled languages like Swift – and in fact, Test Driven Development: By Example (the book to read on TDD) uses Java, a compiled language, to show you how to do TDD. As long as you treat compiler errors the same way you’d treat test failures in an interpreted language, the TDD process is exactly the same.

Going further

You can get the code from the example above on GitHub.

If you’d like to see this project in action on a real project, you can watch this TDD refactoring screencast.

If you’re ready to get started with Swift, join the free Swift course below.