How do you know if your code works? You test it of course. But, how do you test your code? Do you run it in the simulator? On your device? Is that enough? The simple answer is: No. How can we do better? Answer: Unit testing!
It is a well established software engineering maxim that any bug you find while still in development is less costly than a bug found in QA, which is still less costly than if a customer finds it. And cost here is not measured only in dollars of lost engineering time (when found by engineering or QA). If a customer finds your bugs, you may lose them as a customer or at the very least, you might lose their trust in your ability to produce quality software. Enter: Unit testing.
Unit testing is not a new concept, and I am not the first person to write about it. And this will not be an exhaustive treatise on the subject; there are plenty of those to be found by the Googles.
No, the purpose of this post is to sing the praises of unit testing, having been recently re-introduced to it in the context of my daily work, iOS development. I recently read the book Test-Driven iOS Development, by Graham Lee. Simply put, this is an excellent book, for several reasons. I highly recommend it.
The book is devoted to a single topic and is focused solely on that topic for iOS. Once you’ve read a couple “general purpose” books about “writing iOS apps”, which tend to include single chapters on lots of topics (which is great for beginners), one tends to want expanded tomes on individual topics after learning the basics. This book delivers.
More than a book about unit testing however, the book is a fantastic introduction to test driven development, or TDD. Simply put, TDD is the idea that given a set of requirements, you can discern the components of the system you must build, and what those components should do. Given that, you presumably should be able to write your tests first because you know what each component should do. Then, you write the code. Once your tests pass, you’re done, and you have written no more and no less code than you needed in order to satisfy the requirements. The end result is that you have a well tested solution that meets the original specifications with no bloat of unused code. It’s a great approach, but can take some getting used to if you’re not accustomed to thinking this way. The book does a really great job of building a non-trivial app from scratch, tests first.
Now you may be thinking: I have a huge code base already. I can’t possibly start unit testing all that! And you’d be correct, for the most part. True, it would be fool hardy to drop everything and start writing unit tests. Talk about a daunting task! But that doesn’t mean unit testing is out of reach. The approach I thought of while reading the book, and in fact that Lee suggests in one of the final chapters, is that for existing projects what makes sense is to introduce unit testing opportunistically. That is, for new code, or existing code you have to fix or modify, write unit tests. What is important to know about this approach is that a fair bit of refactoring may be required to isolate the thing you want to test. That’s OK, because your code will ultimately become more testable, maintainable, and verifiable. (Note that using TDD from the outset allows you to refactor with confidence after your tests work, because you know you had a working solution before you started to refactor. If you break the code, you’ll know because your unit tests will start failing!)
In my day job, we have introduced unit testing to our existing project, a very complex iPad app. At first, it seemed daunting, because there are some modules that are hellishly big. But once I got into it, things started rolling. I began with the “low hanging fruit”: I wrote unit tests for some existing utility classes that were easy to test in isolation. Yay! We had some unit tests. The next challenge came when I had a simple little bug to fix. I thought about the fix, and then how to write a test. It turned out that my first approach, which was to add a method to a class that was supplying the data I needed, would not work, because the class in question was itself not testable in isolation. I found a better solution that did two things: created a solution that yielded a reusable and general category method, and allowed me to test it in isolation. I knew then, I could use the new category method where I needed to, in a class that was otherwise untestable in isolation in its current form.
My team and I know we have a lot of work ahead of us to improve our code coverage in terms of our unit testing. And, we have a challenge to change our work flow so that we think about testing first, not last.
And that is really the big take away: Think about testing first (and write the tests first), because in the end, it will save a lot of time and money down the road.