Can you TDD networking requests? Sure! It’s just a matter of using Dependency Injection (DI).
But first, a quick recap. Remember this design?
We want a Service class. Now when I began using this style, I made a mistake: I created a single Service class to house an entire API. This violates the Single Responsibility Principle.
The Marvel Browser may end up supporting only one API call. But I’m afraid naming it MarvelService would lead people down the wrong road. We are fetching comic book characters. So let’s use a narrower name: FetchCharactersMarvelService.
Remember: Smaller, focused classes are easier to manage than larger, godlike ones.
Let’s TDD it!
[This post is part of the series TDD Sample App: The Complete Collection …So Far]
Remember this code?
The question I asked in Method Swizzling and the Problems It Hides was about the
[NSURLSession sharedSession] singleton in the first line. Is there a way to control it for unit tests?
By now, you can probably guess my answer: Dependency Injection. But which form?
Constructor Injection is the preferred form of Dependency Injection, because it makes dependencies explicit. Use it when you can.
In our case, this means injecting the NSURLSession as an initializer argument. That doesn’t mean every caller needs to pass in
[NSURLSession sharedSession]. Yes, the designated initializer will take an NSURLSession. But we can always make a convenience initializer later. (Or in Swift, we can provide a default argument.)
Using OCMockito, we create a mock NSURLSession. We want to pass this to an as-yet nonexistent initializer. Let’s write a test that doesn’t even have a real name, just to get started:
Since this doesn’t compile, we stop. Switching from test code to production code, we write a bare-bones initializer:
We know from our design that we want to pass in a Request Model, which we defined as a Value Object. Let’s create one with dummy parameters, and pass it to an as-yet nonexistent method:
This drives the initial creation of the -fetchCharacters: method. Sure, it’s initially empty. But this step forces us to set up the skeleton, and make sure everything compiles. In effect, we’re solving the wiring first, before we tackle the guts.
Now we’re ready for our first assertion! Let’s confirm that -fetchCharacters: asks the NSURLSession to create a data task. This is where the mock object comes into play. But we’re not particular about any of the data task parameters, so we’ll use OCHamcrest’s anything() matcher. Oh, and let’s name the test.
This test successfully runs, and successfully fails.
Now we need a way for -fetchCharacters: to access the NSURLSession passed to the initializer. We can do this with an instance variable or a property. My preference these days is to use a hidden, read-only property:
The simplest code to pass the test is
But these days, that gives me “non-null argument” warnings. So let’s give it something: a dummy URL, and an empty block.
That passes! We’ve taken the hardest step: getting started.
Of course, foo://bar isn’t the URL we want. Let’s write a test to target the Marvel Comics API. First, let’s get the host right. I copy-and-paste the first test, this time with a different OCHamcrest matcher:
hasProperty(@"host", @"gateway.marvel.com") confirms the host value of the URL parameter. Getting this to pass is trivial.
Now we have something worth refactoring. The two tests are very similar to each other. Normally, I’d start extracting a test fixture.
But a good refactoring question to ask is, “Does this test still add value?” The first test verifies a call to the mock, with any parameters. The second test verifies the same call, with narrowed parameters.
Let’s just delete the first test.
This is a valid thing to do. Sometimes, the scaffolding is temporary.
To write a test that ensures we are using https, I copy-and-paste the “host” test. The matcher changes to
hasProperty(@"scheme", @"https"). We change the test name to testFetchCharacters_ShouldMakeDataTaskWithSecureConnection.
The implementation is trivial.
Of course, I’m just getting warmed up here. We want to test the URL path. We want to verify the translation of the Request Model into URL parameters. We need to include the Marvel’s authentication. And we need to start the data task.
But I hope I have shown you a few things:
What can you inject in your code to enable testing? What forms of Dependency Injection have you used? Click here to share your thoughts, experiences and questions.[This post is part of the series TDD Sample App: The Complete Collection …So Far]
Jon is a coach and consultant on iOS Clean Code (Test Driven Development, unit testing, refactoring, design). He’s been practicing TDD since 2001. You can learn more about his background, or see what services he can bring to your organization.