(This is the first part of a text-based version of the talk I gave for iOS Con at Skillsmatter in London on Friday 16th May. If you prefer the full multimedia experience, there’s video available behind a login wall at https://skillsmatter.com/skillscasts/5167-tdd-in-ios.)
The presentation slides are available here.
Why is testing scary?
Unit testing is a topic that gets talked about a lot, but if you’re not a computer scientist it can have a tricksy reputation. It doesn’t help that much of the source material available is Java-based. That’s fine if you like Java - I’m personally not so keen - but there’s a lot less help if your primary weapon of choice is Objective-C.
Testing is also a topic that attracts - how shall we put it? - some of the more tedious personalities in this business. There’s nothing so dull as a self-appointed “thought leader”, and a lot of what passes for debate on testing is so much arcane, “how many angels can dance on the head of a pin”-style nonsense.
That’s got very little to do with the day-to-day grind of shipping software. There is no One Way to do this, and if you look hard enough at the motives of those who would have you believe that, you often find it comes down to selling themselves as a brand.
Why is testing important?
All that said, testing is important. Let’s start from the premise that the fewer bugs in your code, the better. If you subscribe to that school of thoughy, then anything that helps you achieve this has to be a good thing. As the developer who writes 100% bug-free code hasn’t been born yet, we’re also faced with the challenge that the more code we write, the more likely it is that the project will turn out to be infested with them.
Documentation is also held to be a Good Thing, but it’s also something that very few devs are particularly keen on doing - especially once the code’s been written. A comprehensive test suite, especially if it’s been built using one of the more “descriptive” tools such as Kiwi or Specta, can almost replace documentation. It’s also considerably easier to read than code, because it’s documenting intent rather than execution - the “what” rather than the “how”.
Perhaps the most important reason for taking a test-driven approach, though, is the way you’ll knit yourself a “security blanket” around your code. We’ve all been in the situation where making changes to an existing code base is a stressful affair, because you’re never quite sure whether changing something over here will break something over there.
If your test suite is comprehensive enough, you can relax to an extent knowing that the tests will catch those kinds of problem. And that becomes particlarly important if you’re working with others, because tests can help catch things that break your code.
The basic purpose of testing is to ask the question “does my code do what it’s supposed to do.” Assuming that you can give a positive answer to that, it will then help you to ask other, more probing questions. *“Can my code handle unexpected values?” is one of them. Coding for the “happy path” only is a common problem - how can you ensure that your app will still work if the API doesn’t respond, for example. What will happen if the data received back from the API is beyond the bounds you expected, or is corrupt?
Even if your code is capable of handling strange data, you can still end up creating problems for yourself further down the line. As you add more classes and features, the chances of something new breaking something existing multiplies. Protecting each area with tests means that you have a safety net that should catch problems by code in another part of the app.
Why not test last?
The meaning of the word “test” implies checking after the fact - making sure that the code you’re written functions as you expected it to do. The problem with testing code after you’ve written it is that there’s actually very little motivation to do this in normal circumstances. After all, you’ve written the code, it does what it’s supposed to, and you’re infallible - right? Testing that it works is just spending more time doing over the same ground.
That’s definitely the response you’ll get when the project manager wanders over and asks what you’re doing. If the answer is “writing tests for the feature that we shipped last week”, then they’re going to ask when you’re going to get on with more productive stuff. In a time-constrained project, there’s a perverse incentive not to test.
Why test first?
The rhythm of test-first, or test-driven development follows a predictable pattern. First, you write a test which describes the outcome that you’re after. What that will be obviously depends on the type of code you’re writing - in the case of a calculation, it would be a correct result; in the case of a UI feature it would be some kind of update to the views.
Then, you run the test. This seems counter-intuitive, because it fails. It has to fail, after all - you haven’t written any code to make it pass yet. In the jargon, your test has just “gone red”. But in the process of seeing the test fail, you’re already getting clues about how to go about fixing it. It’s like having a small and benign homunculus perched on your shoulder, whispering hints about what to do as the tests progress.
With that advice in mind, you can then write the code that will make the test pass - or “go green”. If at first the test fails, you know you haven’t got it right yet - but you always have the target in mind, because you started by describing the outcome that you were looking for.
Once you’ve got a passing test, you’re then free to improve things safe in the knowledge that the test will catch anything you do to break the code. That’s the “refactor” step, and you’ve now been once round the red-green-refactor loop that is the basic process of test-driven development.
Challenges of testing iOS
Test-driven development, or indeed testing of any kind, has some special challenges in the iOS world. The tools and techniques of testing were developed in a terminal-driven world, so many of the approaches are predicated on the results being something that has no user interface component.
On iOS, on the other hand, practically everything happens in response to some kind of user input passed down through the Cocoa Touch layer. And the code that responds to the touches can be highly modular - an indidivual class rarely stands and operates on its own, but instead has to collaborate with all manner of other classes and external APIs.
These two factors - responding to touches, and highly-modular code architectures - can make test-driven development on iOS seem like an impossible challenge. That’s even more the case if you’ve inherited an existing project, and you’re now faced with the challenge of wrapping tests around an already-built code base.
Fortunately, there are some available tools and techniques which can get around both these problems. In the next post, I’ll dig down into how these work in practice, and how you can take advantage of the structure of Cocoa Touch itself to build apps in a completely test-driven way.