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.