TDD Networking with DI: Is It Really That Easy?

Shares

Can you TDD networking requests? Sure! It’s just a matter of using Dependency Injection (DI).

But first, a quick recap. Remember this design?

Web request: Clean Architecture

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]

Networking: The unit testing challenge

Remember this code?

NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *dataTask = [session dataTaskWithURL:url
                                        completionHandler:completion];
[dataTask resume];

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.)

Starting the Constructor Injection

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:

- (void)testFoo_ShouldBar
{
    NSURLSession *mockSession = mock([NSURLSession class]);
    QCOFetchCharactersMarvelService *sut = [[QCOFetchCharactersMarvelService alloc] initWithSession:mockSession];
}

Since this doesn’t compile, we stop. Switching from test code to production code, we write a bare-bones initializer:

- (instancetype)initWithSession:(NSURLSession *)session

{

    self = [super init];

    return self;

}

TDD the Service method

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:

- (void)testFoo_ShouldBar

{

    NSURLSession *mockSession = mock([NSURLSession class]);

    QCOFetchCharactersMarvelService *sut = [[QCOFetchCharactersMarvelService alloc]
            initWithSession:mockSession];

    QCOFetchCharactersRequestModel *requestModel = [[QCOFetchCharactersRequestModel alloc]
            initWithNamePrefix:@"DUMMY" pageSize:10 offset:30];



    [sut fetchCharacters:requestModel];

}

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.

- (void)testFetchCharacters_ShouldAskURLSessionToCreateDataTask
{
    NSURLSession *mockSession = mock([NSURLSession class]);

    QCOFetchCharactersMarvelService *sut = [[QCOFetchCharactersMarvelService alloc]
            initWithSession:mockSession];

    QCOFetchCharactersRequestModel *requestModel = [[QCOFetchCharactersRequestModel alloc]

            initWithNamePrefix:@"DUMMY" pageSize:10 offset:30];



    [sut fetchCharacters:requestModel];



    [verify(mockSession) dataTaskWithURL:anything() completionHandler:anything()];
}

This test successfully runs, and successfully fails.

Finishing the Constructor Injection

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:

@interface QCOFetchCharactersMarvelService ()
@property (nonatomic, strong, readonly) NSURLSession *session;
@end



@implementation QCOFetchCharactersMarvelService


- (instancetype)initWithSession:(NSURLSession *)session
{

    self = [super init];

    if (self)

        _session = session;

    return self;
}

Starting the Service method

The simplest code to pass the test is

- (void)fetchCharacters:(QCOFetchCharactersRequestModel *)requestModel
{

    [self.session dataTaskWithURL:nil
                completionHandler:nil];

}

But these days, that gives me “non-null argument” warnings. So let’s give it something: a dummy URL, and an empty block.

    NSURL *url = [[NSURL alloc] initWithString:@"foo://bar"];

    [self.session dataTaskWithURL:url
                completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {

    }];

That passes! We’ve taken the hardest step: getting started.

Test: Confirm the host

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:

- (void)testFetchCharacters_ShouldMakeDataTaskForMarvelComicsAPI
{

    NSURLSession *mockSession = mock([NSURLSession class]);

    QCOFetchCharactersMarvelService *sut = [[QCOFetchCharactersMarvelService alloc]
            initWithSession:mockSession];

    QCOFetchCharactersRequestModel *requestModel = [[QCOFetchCharactersRequestModel alloc]
            initWithNamePrefix:@"DUMMY" pageSize:10 offset:30];



    [sut fetchCharacters:requestModel];



    [verify(mockSession) dataTaskWithURL:hasProperty(@"host", @"gateway.marvel.com")

                       completionHandler:anything()];
}

The matcher hasProperty(@"host", @"gateway.marvel.com") confirms the host value of the URL parameter. Getting this to pass is trivial.

Refactoring: Delete the first test

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.

Ensure secure connection

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.

We’re on our way

We now have two tests we want to keep. Some refactoring is in order. Next time, we’ll look at why it’s important to refactor tests, and then how to do so with a 5-minute screencast.

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:

  • How to use Constructor Injection for networking
  • That we can get there using Test Driven Development
  • That this opens the door to a variety of tests that aren’t hard to write

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]

About the Author Jon Reid

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.

follow me on:

Leave a Comment:

12 comments
Add Your Reply