I’ve been unit testing view controllers for as long as I’ve worked in iOS. My screencast How to Do UIViewController TDD is for folks who want to do TDD, but couldn’t figure out how in a programming model centered around view controllers.
But for every person who wants to know how, there are others who question the whole idea. They wonder if unit testing view controllers is worth it at all. So this time let’s skip the “how” and focus on the “why”.
One reader asked me how to win over a team lead who is test-reluctant. The team lead wanted the reader to stop spending time writing unit tests for view controllers. “He questioned why I unit test UI when it seems to take a long time and does not seem necessary.”
I can relate to that. Even on my last job, I had to fight for permission to write unit tests against view controllers! The permission granted was only partial at first: I was allowed to write such tests as long as they were kept in a target that wasn’t run on the build server. This was a major pain for me! It meant that any time a developer made a change that broke my tests, neither they nor I knew about it. (After a couple of months, I finally moved these tests to the regular test target.)
So the question of whether one should write unit tests against view controllers is either a question you yourself ask, or one you will be asked. I have good reasons for what I do. But the real benefit is in the reason behind the reasons. Read on, and I’ll show you what I mean.
Objections to testing view controllers
I’ve run across a couple of common objections to unit testing view controllers. And not just view controllers, but testing interactions with any system APIs.
“It doesn’t make sense to test UI that way.”
It’s important to understand that unit tests don’t test UI. They test code. And that can certainly include UI code.
People who make this objection usually have an alternative in mind. Let’s look at those in the next section.
“The test code is too similar to the production code.”
It can look that way, especially at first. We’ll get to this in “the reason behind the reasons”. But let’s assume for a moment that the test code remains tightly parallel to the production code. That means we’re basically doing everything twice. You might assume this is a waste of time & effort.
Now I want you to tell anyone who does accounting that double-entry bookkeeping is a waste of time. Honestly, you have to enter everything twice? …But this technique has helped accountants find errors for centuries.
Alternative approaches to testing view controllers
What testing alternatives are there?
“If something breaks in the UI, someone will notice.” Well, that depends. If you’re relying on dogfood testing, it depends on whether anyone triggers the problem. A glitch on the main screen will be noticed as soon as dogfooders update to the latest build. A glitch on something deep in the navigation hierarchy could go unnoticed for weeks.
But let’s assume it will be noticed immediately. Even then, consider how long the defect remains in play:
- Time from when the bad commit is merged to when a new build is delivered
- Time from build until dogfooders actually update
- Time to diagnose the problem
- Time to fix the problem
- Time until a good build is rolled out
If you have manual testing scripts, we can probably reduce the time that a defect will go unnoticed. But manual test scripts are labor-intensive and dull. Besides, it takes time to go through the repetitive motions of the manual scripts.
Most people quickly look for alternatives to manual testing. At the very least, we’d all like to reduce it. The immediate reaction of most team leads is to say, “Then let’s use GUI testing to automate those manual scripts. If a system like Xcode UI Testing supports record-and-playback, even better! We can literally hit record, do the manual tests once, and it’ll all be automated.”
Oh, how many times must we replay this scenario? The outcome is always the same: Initial excitement gives way to frustrated gnashing of teeth. It it The Way of Pain. The results are mostly unhelpful. But why, when the cost of initial creation was so low?
- Maintenance cost is high. Any UI changes often. Every little change will break one or more existing GUI tests. These failures often cascade, wiping out entire suites until the tests are updated to match the new UI.
- Slow to run. My gosh, GUI tests are S-L-O-W. So slow, that developers won’t run them while coding. The feedback will arrive only when a build-and-test server finishes. That’s if you’re lucky and the job didn’t die midway.
- Poor signal. A test fails. What went wrong? We don’t know! So someone needs to spend time diagnosing the failure. It may be a real problem in the production code, somewhere in this massive view controller. Or it may just be another maintenance change.
I’m painting GUI testing in a bad light, but someone has to wave a red flag here. I’m not saying GUI testing is terrible. I’m saying there are high costs. And I know there are ways to mitigate these costs, such as Page Objects.
I also recognize that maintenance cost, feedback time, and signal value are issues that affect all types of testing, including unit tests. So let’s look at costs vs. benefits.
Costs vs. benefits of unit testing view controllers
What are the costs of unit tests? Well, there’s the initial time it takes to write one.
For well-structured code, it’s easy to write a new unit test. For code with tangled dependencies, it can be a process of yak shaving. The easiest way to avoid these yaks is to learn Object Oriented Design and practice Test Driven Development.
What about maintenance? There are times when a benign change requires us to modify our tests. But by refactoring test code to keep it DRY (or at least “damp”), we reduce repeating points of change. Ideally, we can reduce the needed change to just one place.
So what are the benefits of unit tests?
- They’re fast. In fact, they’re so fast that the rules change. More on this below in “the reason behind the reasons”.
- They’re precise. A well-written test will report with such precision and detail that you don’t need to step through code in a debugger. (Whenever I do use the debugger, I try to ask myself if there’s a way to report the information automatically.) Quite often, a well-formed test name will tell you enough about the defect, just from the name.
Rules change with high-speed tests
The goal of any test suite is to prevent regressions. The speed & precision of unit tests alone make them worth investing in.
But something magical happens when they’re that fast. …How often do you run tests?
When tests are slow (I’m looking at you, GUI testing), developers won’t run them on their local machines. No, they’ll do all their development up front, then offload the testing to the build-and-test servers.
But when tests are fast, how often can you run them?
Frequently. So frequently, that you can get fast feedback at every stage of your change.
In fact, you can run unit tests so often that the cost of diagnosing a test failure can shrink to nothing. When a test fails, don’t try to figure it out. Just undo.
The cost of any defect drops dramatically. Consider the cost of fixing a defect that makes it out to dogfood. Think of the number of people involved. Think of the time spent to file a bug report. Think of how the bug is triaged and assigned. Think of how the assigned developer needs to first reproduce the problem, then diagnose it. Then the change needs to be approved. Finally, the fix needs to be verified.
But what if the tests were passing one minute ago? And now there’s a failure? Undo!
The reason behind the reasons
Oh, but I’m not done here. There’s more, there’s more. For me, this is merely technology that empowers a technique: Refactoring.
Refactoring is the reason behind the reasons.
Let’s come back to an objection: when interacting with a system API, test code is tightly parallel to production code.
Well, maybe at first. But it doesn’t stay that way. Refactoring empowers you to change the design of code.
Usually, these look like little changes. A small change here, a small change there. Then suddenly, a larger pattern clicks into place. New design possibilities emerge that you couldn’t see before.
This is true of all refactoring. But for a moment, let’s focus on view controllers. Let’s say you have a view controller with thorough unit tests. What refactoring is possible? Well, you can shear off areas of responsibility into separate classes, each conforming to the Single Responsibility Principle. One of those classes can be a Presenter which knows how to take your model and call back to your view controller with model-agnostic requests. This moves all awareness of model semantics outside the view controller!
Refactoring from MVC to Model View Presenter is an example of something you would be empowered to do by unit testing view controllers. And not in a giant so-called refactoring which is really “let’s rewrite it, then see if it still works.” No, I’m talking piece by piece in small, verified steps.
Because what do you do if anything goes wrong with a small step? Undo.
How to convince your team lead
Graham Lee and Orta Therox offer much shorter statements about testing:
Remember that the only code your tests need to cover is any code you require to work.
— Graham Lee (@iwasleeg) June 8, 2015
Having a pretty reasonable test coverage makes making large sweeping changes inside a section of code significantly easier.
— 💍rta Therox (@orta) June 9, 2016
Few people are ever convinced by arguments, but you never know. Those tweets, and this blog post, may lead to a fruitful discussion. Most engineers are skeptical of anyone who offers The One True Path. But they are open to discussing costs vs. benefits.
My general approach isn’t to showcase the unit testing side of things. I don’t mind hiding it. If necessary, I’ll even put up with having my tests in a separate target that everyone else ignores — for a while. The main thing I want to show is how quickly & effectively I can refactor. In my experience, this is what people find most convincing.
Go for it, friends.
Where in your code do you feel empowered to make bold refactoring changes? Where not so much? Please share in the comments below.