.st0{fill:#FFFFFF;}

But Does It Work? Boost Confidence with Acceptance Tests 

 August 4, 2015

by Jon Reid

2 COMMENTS

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.

Code snippet sample

Improve your test writing “Flow.”

Sign up to get my test-oriented code snippets.

We’ll do that with an acceptance test.

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? Let us know in the comments below.

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

Tests should express what is important, and hide what is unimportant.

Click to Tweet

Jon Reid

About the author

Programming was fun when I was a kid. But working in Silicon Valley, I saw poor code lead to fear, with real human costs. Looking for ways to make my life better, I learned about Extreme Programming, including unit testing, test-driven development (TDD), and refactoring. Programming became fun again! I've now been doing TDD in Apple environments for 20 years. I'm committed to software crafting as a discipline, hoping we can all reach greater effectiveness and joy. Now a coach with Industrial Logic!

  • 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));

  • {"email":"Email address invalid","url":"Website address invalid","required":"Required field missing"}
    >