Test-driven development, or TDD, is an iterative way to develop software by iteratively making many small changes backed by tests.
It has four steps:
- Write a failing test
- Make the test pass
This is called the TDD Cycle. It ensures you thoroughly and accurately test your code because your development is… driven by testing!
By writing a test followed by the production code to make it pass, you ensure your production code is testable and that it meets all of your requirements during development. As an added bonus, your tests act as documentation for your production code, describing how it works.
On the surface, the TDD process seems pretty simple. Well, I’m sorry to tell you that… wait, it actually is really simple!
Sure, there are special circumstances for how to implement this cycle at times, but that’s where this book comes in! Once you get the hang of this process, it will become second nature. You’ll learn a lot more about this process in the next chapter.
Why should you use TDD?
TDD is the single best way to ensure your software works and continues to work well into the future — well, that’s quite a bold claim! Let me explain.
It’s hard to argue against testing your code, but you don’t have to follow TDD to do this. For example, you could write all of your production code and then write all of your tests. Alternatively, you could skip writing tests altogether and, instead, manually test your code. Why is TDD better than these options?
Good tests ensure your app works as expected. However, not all tests are “good.” Writing tests for the sake of having tests isn’t a worthwhile exercise. Rather, good tests are failable, repeatable, quick to run and maintainable.
TDD provides methodology that ensures your tests are good:
The first step is to write a failing test. By definition, this proves the test is failable. Tests that can’t fail aren’t very useful. Rather, they waste valuable CPU time.
Before you’re allowed to write a new test, all other previous tests must pass. This ensures that your tests are repeatable: You don’t just run the single test you’re working on, but rather, you constantly run all of the tests.
By frequently running every test, you’re incentivized to make sure tests are quick to run. All of your tests should take seconds to run — preferably, one second or less.
A single test that takes a hundred milliseconds is too slow: After only ten tests, your entire test suite will take one second to run. After fifty tests, it takes five seconds. After several seconds, no one runs all of the tests because it takes too long.
When you refactor, you update both your production and test code. This ensures your tests are maintained: You’re constantly keeping them up-to-date.
By iteratively writing production code and tests in parallel, you ensure your code is testable. If you were to write tests after completing the code, it’s likely the production code would require quite a bit of refactoring to fully unit test.
Nonetheless, the devil’s advocate in you may say, “But you could write good tests without following TDD.” You definitely could, but you may struggle to succeed. You can definitely do it in the short term, but it’s much more difficult in the long term. You’d need to be disciplined about writing good tests. Before long, you’d likely create some sort of system to ensure that you’re writing good tests… you’d likely find yourself doing a variant of TDD!
What should you test?
Better test coverage doesn’t always mean your app is better tested. There are things you should test and others you shouldn’t. Here are the do’s and don’ts:
Do write tests for code that can’t be caught in an automated fashion otherwise. This includes code in your classes’ methods, custom getters and setters and most anything else you write yourself.
Don’t write tests for generated code. For example, it’s not worthwhile to write tests for generated getters and setters. Swift does this very well, and you can trust it works.
Don’t write tests for issues that can be caught by the compiler. If the tested issue would generate an error or warning, Xcode will catch it for you.
Don’t write tests for dependency code, such as first- or third-party frameworks your app uses. The framework authors are responsible for writing those tests. For example, you shouldn’t write tests for
UIKitdevelopers are responsible for writing these. However, you should write tests for your custom subclasses thereof: This is your custom code, so you’re responsible for writing the tests.
An exception to the above is writing tests in order to determine how a framework works. This can be very useful to do. However, you don’t need to keep these tests long term. Rather, you should delete them afterwards.
Another exception is “sanity tests” that prove third-party code works as you expect. These sort of tests are useful if the library isn’t fully stable, or you don’t trust it entirely. In either case, you should really scrutinize whether or not you want to use the library at all — is there a better option that’s more trustworthy?
But TDD takes too long!
The most common complaint about TDD is that it takes too long — usually followed by exclamation point(s) or sad-face emojis.
Fortunately, TDD gets faster once you get used to doing it. However, the truth is that compared to not writing any tests at all, you’re writing more code ultimately. It likely will take a little more time to develop initially.
That said, there’s a really big hole in this argument: The real time cost of development isn’t just writing the initial, first-version production code. It also includes adding new features over time, modifying existing code, fixing bugs and more. In the long run, following TDD takes much less time than not following it because it yields more maintainable code with fewer bugs.
There’s also another cost to consider: customer impact of bugs in production. The longer an issue goes undiscovered, the more expensive it is. It can result in negative reviews, lost trust and lost revenue.
If an issue is caught during development, it’s easier to debug and quicker to fix. If you discovered it weeks later, you’d spend substantially more time getting up to speed on the code and tracking down the root cause. By following TDD, your tests ultimately help safeguard and protect your app against bugs.
When should you use TDD?
TDD can be used during any point in a product’s life cycle: new development, legacy apps and everything in between. However, how and where you start TDD does depend on the state of your project. This book will cover how to approach many of these situations!
However, an important question to ask: Should your project use TDD at all?
As a general rule of thumb, if your app is going to last more than a few months, will have multiple releases and/or require complex logic, you’re likely better off using TDD than not.
If you’re creating an app for a hackathon, test project or something else that’s meant to be temporary, you should evaluate whether TDD makes sense. If there’s really only going to be one version of the app, you might not follow TDD or might only do TDD for critical or difficult parts.
Ultimately, TDD is a tool, and it’s up to you to decide when it’s best to use it!
In this chapter, you learned what TDD is, why you should use it, what to test and when to use it. Here are the key points to remember:
TDD offers a consistent method to write good tests.
Goods tests are failable, repeatable, quick to run and maintainable.
Write tests for code that you’re responsible for maintaining. Don’t test code that’s automatically generated or code within dependencies.
The real cost of development includes initial coding time, adding new features over time, modifying existing code, fixing bugs and more. TDD reduces maintenance costs and quantity of bugs, often making it the most cost effective approach.
TDD is most useful for long-term projects lasting more than a few months or having multiple releases.