Before I start the first networking calls in the iOS TDD sample app, I have a question for you.
How would you write unit tests for this code?
NSURLSession *session = [NSURLSession sharedSession]; NSURLSessionDataTask *dataTask = [session dataTaskWithURL:url completionHandler:completion]; [dataTask resume];
This fires off a request to download some data, then does something with that data. Most people would write the code pretty much this way. (Of course, supplying a URL and a completion block.)
Then they ask, “How do I unit test this?”
Think about it for a minute. What are your options?
The third line depends on the second line, which creates
dataTask. That in depends on the first line, which creates
session. And that comes from a singleton,
Oh, great. A singleton.
Is there a way to control it for unit tests? What are your options?
At this point, an advanced Objective-C programmer might say, “We can swizzle it.”
Method swizzling is the act of reaching into a class during runtime. You replace one method with another. It’s magic, made possible by a dynamic language.
We can reach right into NSURLSession and replace
sharedSession with a stub that we control. Then you don’t have to change the production code.
Instead of doing your own method swizzling, there are libraries that can help. In particular, there are libraries geared specifically for testing.
A general-purpose approach is to use OCMock. When you use OCMock to stub a class method, it’s actually swizzling a replacement for the class method.
A more specialized approach is to use libraries that stub particular features. Stubbing-via-swizzling libraries include:
Stubbing via method swizzling means you don’t have to change production code. This makes it helpful for legacy code.
But even though I wrote the last two libraries in that list, I need to wave a caution flag…
Method swizzling for stubbing is a form of Dependency Injection called Ambient Context. (Note: There are also other ways to do Ambient Context.) Like I said, it’s helpful for legacy code. But there’s a problem.
The problem with Ambient Context is that it hides the dependencies.
Dependency Injection isn’t just about “making something testable.” It’s a form of design, with its own feedback loop. When things feel awkward “because now I have to inject all these things (grumble),” we should pay attention to those feelings.
In TDD, we say, “Listen to the tests.” That’s a tighter version of a larger Object Oriented Design concept: “Watch out for code smells.”
So if you’re feeling awkward because a piece of code has too many dependencies… good. That feeling is telling you something important. Maybe it’s “Stop Making Big Classes.” Pay attention to the discomfort. Don’t sweep it under the rug.
I’ve spoken and written about Dependency Injection before. There are several ways to approach it. There are even different ways to do Ambient Context — especially on a class you own. Ambient Context can be appropriate for cross-cutting concerns, such as logging.
So the next time you’re staring at some code wondering, “How do I unit test this,” I want you to pause. Brain teaser puzzles have hidden breakthroughs.
Here’s the big “Aha!” idea for unit testing: Break free of the idea that you can’t change production code to make it testable.
Agree? Disagree? What other stubbing-via-swizzling libraries have you used? How has wrangling dependencies improved your code? Share your thoughts and experiences in the comments below.
I first experienced the joy of programming in junior high. But on the job, some of that joy was sucked away by seeing code my teammates were afraid to touch. Poor code led to fear, and fear led to our entire team being let go. I began searching for ways to improve code. I stumbled upon the first wiki, which was about Design Patterns, Extreme Programming, and Test Driven Development (TDD). I rediscovered joy on the job. I've now been doing TDD in Apple environments for 17 years. I'm committed to software crafting as a discipline, with the hope of raising you, my fellow programmers, to greater effectiveness and joy.
Please log in again. The login page will open in a new window. After logging in you can close it and return to this page.