Many programmers assume that test-driven development (TDD) doesn’t work well for iOS development. This ill-founded assumption really comes from a lack of any experience with TDD. But because iOS developers learn most of their chops by referring to other people’s code, it also comes from a lack of helpful examples.
Disclosure: The book links below are affiliate links. If you buy anything, I earn a commission, at no extra cost to you.
So I was really glad when Graham Lee’s book Test-Driven iOS Development came out. Finally, something I can point others to besides my code!
Chapter 1 starts off with a surprising answer to the question, “Why write unit tests?” Why, to make more money! It shows how the traditional handoff-to-QA form of testing comes from the waterfall model of development, and how the cost of fixing defects increases with each phase of the waterfall.
Chapter 2 lays out the principles of test-driven development: test first, writing “Just Barely Good Enough” code that satisfies the test, and refactoring. But it also describes an important principle from Extreme Programming: “Ya Ain’t Gonna Need It,” or YAGNI. Basically, code only what you need to. This keeps production code simple, and avoids wasting time writing code that won’t have any effect.
The book goes on to introduce unit testing by showing how one might write tests without a unit testing framework — the old-fashioned way! Then we get an overview of the more common tools. Finally, we hit the meat of the book: a full example of creating an iOS app using TDD, spanning five chapters.
My a-ha! moments
The big example not only demonstrates TDD, but also shows how the Single Responsibility Principle improves design. I had two big a-ha! moments reading this code:
- All model code I’ve seen includes code to build that model from XML or JSON data. Graham’s code demonstrates the power of separating model construction from representation. This is a big deal not just for improving cohesion. I’ve run into a model that assumes it’s built from XML, only to find a case where it might be built from JSON instead.
- All table code I’ve seen takes the table data source code and the table delegate code, and lumps it all together with the overall view controller. This is normal even when the view controller isn’t a UITableViewController. Graham’s code goes against the flow by pulling data source and delegate into a separate class. This allows you to use same view controller for different tables.
Where I differ
I do want to point out one thing in the book that is incorrect, and another where I disagree.
Unit test bundles should link just fine
After writing the very first test, there’s a need to create the
Topicclass. On p.68 it says,
Add a new
Topicto the project. When Xcode asks which targets to add the new file to, check both the app target and the test bundle target. (This is required to work around a problem with unit test bundles: They don’t link symbols from their host apps correctly.)
That was once true for iOS development with Xcode 3. But with Xcode 4, Apple brought iOS unit testing in line with Mac unit testing: Xcode links the test bundle against the app, and injects it into the running app. If the test bundle is unable to link, there’s a problem in the build settings. Make sure “Symbols Hidden by Default” is “No” for your app’s Debug configuration.
Update: Graham Lee says, “Sadly, at the time of publication, the linker problem still needed to be worked around.”
Is method swizzling worth it?
The part where I disagree is the author’s use of method swizzling. He acknowledges that some may consider method swizzling “too clever,” and in my case, that may simply be because I haven’t used it yet. But let’s look at the two cases where he felt the need for swizzling.
First, on p.151, he’s trying to come up with a way to TDD that
viewDidAppear: call its superclass. For me though, this feels like overkill because the “invoke super” idiom is well-ingrained in iOS developers. In particular, I don’t want to make testing look overly convoluted to my coworkers, whom I am trying to encourage to write tests. If I need to soften strict TDD to get them to adopt it at all, I’ll do so.
Still, I do have to say I’ve wondered how to enforce “invoke super” from a test. For idiomatic methods like
viewDidAppear: I don’t think this is a big deal. But I may run into another method where it’s less obvious, and needs a test to verify it. Then I’ll know to pull method swizzling out of my toolbox.
The other case where the book uses method swizzling is on pp.154-156. The swizzling replaces a method normally invoked by a notification with a category method that records the notification. The tests verify that the class is observing a notification. But Michael Feathers’ book Working Effectively with Legacy Code shows a different way to replace methods: Subclass and Override.
- In the test code, create a special subclass that is only for testing. (For a class named Foo, I create a subclass named TestingFoo.)
- Write your test to use the testing subclass.
- Override the method.
It’s that easy! And this approach works in any object-oriented language — no swizzling required.
Update: Since writing this, I got into a situation where I needed to write a unit test for code that is tied to a singleton. Can you guess how I altered the singleton for testing? That’s right, I copied Graham’s method swizzling technique straight out of the book! I’d still consider it an unusual tool to be avoided if possible, but method swizzling is definitely part of my toolkit now.
Update 2: OK, I’ve had to use Graham’s technique more than once now. I eat humble pie. As Graham told me in a tweet, “everyone says that’s a gross hack until the moment it solves their problem :-)”
Conclusion: Test-Driven iOS Development is worthy
The book concludes with an overview of coding principles that emerge from TDD, including:
- Design to Interfaces, Not Implementations. This is more important for hand-rolled mocks, because it lets you easily substitute fakes. I find it less important when using mock object frameworks like OCMockito, because the generated mock is pretty close to a perfect stand-in. But designing to protocols isn’t just about substituting fakes; it’s also an important way to break dependencies between classes.
- Tell, Don’t Ask. This is a powerful tool for TDD, with a number of different ways to approach it. I use constructor injection, setter injection, injecting a factory, or factory methods which I subclass-and-override. It all depends, and I sometimes change my mind and switch.
- Prefer a Wide, Shallow Inheritance Hierarchy. I’d extend that beyond inheritance, to all dependencies: prefer a wide, shallow dependency graph. Again, protocols make this possible.
I give Test-Driven iOS Development a big thumbs-up. It fills an important hole in TDD examples, and shows how improved designs emerge from good TDD. Pick up a copy today… but don’t just read it! The act of typing someone else’s code actually reinforces the concepts in your head. And these are concepts that will serve you well.
Question: After working through the book, where is it still difficult to apply TDD to your iOS development? Leave a comment below.