Quality Coding
Shares

But Does It Work? Boost Confidence with Acceptance Tests

Shares

We’ve TDD’d a class that should handle authentication to the Marvel API. It looks good in theory. But how do we know if it really works?

In addition to TDD-ing part of a system, it’s important to get feedback about the system as a whole. So before we go on to write code to request Marvel comic book characters by name, let’s make sure the authentication class works at all.

We’ll do that with an acceptance test.

Acceptance test: Snapping in a jigsaw piece

If I’d been a little more on my game, we could have written the failing acceptance test before beginning our TDD. The acceptance test would drive the TDD from the outside-in. Once the acceptance test passed, we’d know the code was functional. That top-down approach is called Acceptance Test-Driven Development, or ATDD.

But I ended up doing a bottom-up approach. So let’s add an acceptance test now.

[This post is part of the series TDD Sample App: The Complete Collection …So Far]

Keep acceptance tests separate

This acceptance test will do real networking. That will be too slow to keep with the unit tests, because we don’t want to kill the speed of our TDD feedback loop.

So let’s create a new test target called AcceptanceTests. I don’t like having a bunch of Info.plist files, so I set all my test targets to use the same Info.plist. (While writing this post, I decided a better naming scheme would be App-Info.plist and Tests-Info.plist.)

Acceptance test first draft

Here’s the first draft of the acceptance test. It fires off an NSURLSessionDataTask. To avoid relying on actual results which could change as Marvel updates their database, we just check for an HTTP status code of 200 OK, meaning the request was successful:

- (void)testCallToMarvel_ShouldGetStatus200OK
{
    QCOMarvelAuthentication *sut = [[QCOMarvelAuthentication alloc] init];
    NSString *URLString = [@"http://gateway.marvel.com/v1/public/characters?nameStartsWith=Spider"
            stringByAppendingString:[sut URLParameters]];
    NSURL *url = [NSURL URLWithString:URLString];
    NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
    NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfig
                                                          delegate:nil
                                                     delegateQueue:nil];

    __block NSHTTPURLResponse *httpResponse;
    NSURLSessionDataTask *dataTask = [session dataTaskWithURL:url
                                            completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
                                                httpResponse = (NSHTTPURLResponse *)response;
                                            }];
    [dataTask resume];

    assertThatAfter(5, futureValueOf(@(httpResponse.statusCode)), is(@200));
}

Testing asynchronous code

When testing asynchronous code, you need a way to have the test wait for the results (up to a certain timeout).

XCTest now directly supports testing of asynchronous operations. But the XCTest approach feels really cumbersome to me. Instead, I’m using OCHamcrest’s assertThatAfter to wait up to 5 seconds for the asynchronous call to succeed. I find this easier to read. Other third-party testing frameworks offer similar features.

Refactor the test for readability

The test passes: yay! But I’m not satisfied with how the test reads. By their nature, acceptance tests are more complicated than unit tests, so it’s even more important to work on readability. Also, we want the test to resemble the way we want the production code to look.

So instead of having a URLString variable that packages everything together in one shot, I broke off a validQueryMissingAuthentication. Instead of sut (System Under Test), I use authentication. I assemble the two pieces into a validQueryURL. (See 257f6ed.)

I also renamed the test from testCallToMarvel_ShouldGetStatus200OK to the more explicit testValidCallToMarvel_ShouldGetHTTPStatusCode200.

There’s a fair amount of boilerplate networking code there. It’s unlikely to look like that in real code. Let’s extract that into a helper method -startGETRequestToURL:withCompletionHandler:.

Now the test looks like this:

- (void)testValidCallToMarvel_ShouldGetHTTPStatusCode200
{
    NSString *validQueryMissingAuthentication = @"http://gateway.marvel.com/v1/public/characters?nameStartsWith=Spider";
    QCOMarvelAuthentication *authentication = [[QCOMarvelAuthentication alloc] init];
    NSURL *validQueryURL = [NSURL URLWithString:
            [validQueryMissingAuthentication stringByAppendingString:[authentication URLParameters]]];

    __block NSHTTPURLResponse *httpResponse;
    [self startGETRequestToURL:validQueryURL
         withCompletionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
             httpResponse = (NSHTTPURLResponse *)response;
         }];

    assertThatAfter(5, futureValueOf(@(httpResponse.statusCode)), is(@200));
}

Working and readable ⇒ Confident and documented

By seeing this acceptance test pass, my confidence in the authentication code has increased. I know that it works in a real networking call to the Marvel API. So now we’re ready to use it to build the actual calls.

But I don’t want acceptance tests to be a mass of gobbledygook code. Tests should express what is important, and hide what is unimportant. Through better naming and extracting simple helper methods (just one level), I have a test that is more expressive and better as documentation.

Have you written any networking acceptance tests? How have they helped you? Have you refactored the test code to make it more expressive? Click here to let us know.

[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:
Disclosure: The book links below are affiliate links. If you buy anything, I earn a commission, at no extra cost to you.

Leave a Comment:

2 comments
Oleg Pavlichenkov says 7 months ago

Hello everyone!
Assertion for async code changed in OCHamcrest :

assertThatAfter(5, futureValueOf(@(httpResponse.statusCode)), is(@200));

now is:

assertWithTimeout(5, thatEventually(@(httpResponse.statusCode)), is(@200));

Reply
Add Your Reply