TDD Networking with DI: Is It Really That Easy?

February 4, 2016 — 12 Comments

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]

Jon Reid

Posts Twitter Facebook Google+

I've been practicing Test Driven Development (TDD) since 2001. Learn more on my About page.

12 responses to TDD Networking with DI: Is It Really That Easy?

  1. I like your approach, but I have the feeling that what we are doing is a test for internal implementation of this method, not just the result.
    We assert that the fetchCharacters method is calling the dataTaskWithURL with the right parameters. This is true now, but it can be false later, when we change the internal implementation of this method.

    People says that only the public method should be tested, so the test are valid when you refactor the class. If this is true, don’t you think is even more important to do not test the internal implementation of each method?

    Should we just rely on the result of a method or should we test what the method is doing internally?

    • Good question, Ignazio. Anytime you use mocks, you’re verifying an interaction between the System Under Test and whatever is being mocked. In my example, the interaction is with NSURLSession. It’s not a hidden detail, because we are explicitly injecting it. As long as that remains true, we can refactor freely.

      An established app may have a Facade to manage networking. Let’s say we want to switch from direct use of NSURLSession to this Facade. That changes the interaction between objects, so getting there wouldn’t be done through refactoring (even though many people would call it refactoring). No, we would instead TDD our way there by injecting the Facade, and verifying interactions with it.

      At some level, something is going to call NSURLSession. Wherever that happens, I want to show that it can be TDD’d. Facades are great, but I want to TDD them, too.

  2. Thanks for your answer Jon,
    I think sometimes people do not use tests just because they are scared to do it in the wrong way.
    I would like to point you another example: Let’s pretend that now we need a cache mechanism inside the `fetchCharacters`. This should be called “refactoring” because we are not changing the external behavior of the code, but any change in this direction will breaks our test.
    Maybe we should empathise that there are two types of tests, white box and black box. And testing with the mock is definitely a white box tests.
    What do you think about?

    • Unit testing, by its nature, is black-box at the level of the SUT. But if you take even one step back to a larger perspective, the details of all these classes is white-box. Something like caching doesn’t affect the external behavior of the code in an obvious way, from an acceptance test level. But at the SUT level, it’s new behavior that can be fully specified through unit tests.

      The main reason I have unit tests is to support refactoring. So you’re right, it’s important to think about how to avoid tests that lock down the implementation. And yes, interaction testing with mocks includes more information about the implementation. But mocks are necessary any time you want unit tests for:

      • Network
      • Database
      • File system
      • User interface

       
      Maybe that helps explain why I’m mocking in this example.

  3. I totally agree with you. Thanks.

  4. I have a question regarding architecture of a networking system like this. (I think 😀)

    If you were to have a gateway that did several different things related to the same entity (for instance, user sign up, user login, change password, etc…) would you create a service class for each different action so that each service did one specific thing?

    Or would you create multiple gateways?

    Or just create a single gateway and a service class that could process each action?

    Or something altogether different?

    Thanks

    Oliver

    • Oliver, I don’t want a massive “All the APIs” class. Instead, I’d create a separate Service for each piece of functionality. Underneath, they would probably share code in some way (composition or inheritance). That can be discovered later, driven by reality instead of supposition.

  5. Do you really find value in this kind of testing?

    Sometimes I don’t find it useful to write unit tests to verify how your code is interacting with a system API(NSURLSession & friends). I feel that I’m not testing that my code works, but just that my code interacts with that API in the way I think it would work. The same would apply to testing view code. In both cases, I feel I end up having “very similar” code in my tests and production code, and I consider that as a code smell advising maybe that code is not worth to unit test.

    When I deal with networking, I prefer to write a very thin wrapper around those APIs(NSURLSession…), somehow test it manually if necessary, and mock/stub that wrapper in my business classes’ tests.

    • I think that you can write a very thin wrapper around `NSURLSession` with no logic inside and skip the test…but I don’t see how a wrapper like this can useful basically is just a class rename…probably you want to have a wrapper that includes a bit of logic, for example injecting the auth header for each request, and in that case a few short tests can be a nice thing to have.

      It’s my idea that testing with UnitTests gives you a real confidence in your code only if you have a good coverage. If you have UnitTests only for some piece of you code, it’s hard to be confident that everything works correctly.

      • Thanks a lot for your reply, Ignaizo.

        You are right, the class where I wrap NSURLSession does a bit more than just wrapping it, like setting some common headers. I didn’t explain it properly. In my particular case(I use google protocol buffers) this class receives a protobuf representing the request and returns another protobuf with the response in a completion block. This is a special case though, as my server API does not use the HTTP semantics as most RESTful APIs do, and therefore I don’t need to deal with things like constructing URLs from parameters, etc. Basically, the code in that class is pure logicless glue code, and IMO is not worth it to write a unit test for such a class.

        On code coverage, I normally don’t pay much attention to it. I only look for high coverage in my business classes, for the reasons I described in my initial comment: testing how your code interacts with a 3rd party API just verifies how your code interacts with it, not that the code does actually work as expected.

    • Emilio,
      On top of what Ignazio has already said… Although the test code and production code currently looks very similar, it doesn’t have to stay that way. The purpose of tests is to enable refactoring. As long as I have confidence that the system APIs are being called in the way I expect, the production code can be reshaped into any form that makes sense.

Leave a Reply

Text formatting is available via select HTML.

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong> 

*