.st0{fill:#FFFFFF;}

Need Feedback Now? Quickly Hack a Spike Test 

 June 7, 2016

by Jon Reid

0 COMMENTS

You’re building a new network call to a server. The thing that will actually use this call isn’t ready. But you’re anxious to see the call work in a full round-trip to the actual server. Basically, you want to know: does this part of the code work? How can you get the feedback you need?

Answer: Quickly hack together any way that works.

The agile workflow is to build, validate, and adjust course. Extreme Programming teaches us to use many feedback loops. And fast feedback is the primary benefit of TDD. …But TDD isn’t the only way I’ve gotten feedback on my Marvel Browser:

  • I did a spike solution to learn how to do request authentication for the Marvel Comics API.
  • Having TDD’d that authentication, I wrote an acceptance test to ensure that it works.

This time, I want to combine these two approaches. It’s a hybrid for which I don’t have a name. Maybe a “spike test”?

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

The Work So Far

I’ve been building a “fetch characters” network request to the Marvel Comics API. So far, I’ve TDD’d:

There are a couple of things remaining before I’m ready to initiate a request…

Start the Data Task

We’ve created a data task. But we need to start that task. Here’s a test to drive that requirement:

- (void)testFetchCharacters_ShouldStartDataTask
{
    [given([mockSession dataTaskWithURL:anything() completionHandler:anything()])
            willReturn:mockDataTask];
    [sut fetchCharactersWithRequestModel:[self dummyRequestModel]
                          networkRequest:[[QCONetworkRequest alloc] init]];
    [verify(mockDataTask) resume];
}

You may feel funny seeing a mock return a mock. Normally, that’s something to avoid! Having to mock a mock to give you a mock is feedback that the design needs improvement.

But this case isn’t as bad as that. Here, we are using mockSession as a stub, not a mock. And all the stub does is return a (mock) data task.

We then call fetchCharactersWithRequestModel: and confirm that doing so tells the data task to resume.

Time For a “Spike Test”!

Now the code has all the pieces we need to send a “fetch characters” request to Marvel. After refactoring, here’s the Service method:

- (void)fetchCharactersWithRequestModel:(QCOFetchCharactersRequestModel *)requestModel
{
    NSURL *url = [self URLForRequestModel:requestModel];
    NSURLSessionDataTask *dataTask = [self.session dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
    }];
    [dataTask resume];
}

What it lacks is a way to get the response. I don’t know what the response handling should look like without TDDing the internal API. (One of the benefits of TDD is to see what an API will look like from the caller’s point of view.) Until I determine the API, I don’t want to write a full-blown acceptance test.

I want something like a spike solution. Quick and dirty, just enough to give me feedback. But this time, it’s not an experiment to determine a solution. It’s an experiment to see if what we have so far actually works. A spike test.

A "spike test" lets you know if things are working so far, before the code is really done.

Click to Tweet

Artifacts Are Your Friend

A spike test may or may not have assertions. We’re never going to automate it. It’s a one-shot manual test.

We want artifacts of some kind. NSLog will do fine for many spike tests. For something visual, use local variables with a breakpoint. That will let you use Xcode’s Quick Look to render something without having to do all the work.

In my case, here's a hacked-together body of the currently empty completion handler:

NSLog(@"error: %@", error);
NSLog(@"response: %@", response);
NSString *str = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"data: %@", str);

Basically, I’m taking every argument to the completion handler and logging it.

Hack a Trigger

We need a trigger, something to fire off the request. And we need to keep the Service around so that it gets the response.

My main view controller is currently empty. Let’s have it do the work. It will initiate the request when the view loads:

@implementation ViewController
- (void)viewDidLoad
{
    [super viewDidLoad];
    QCOFetchCharactersMarvelService *service =
            [[QCOFetchCharactersMarvelService alloc] initWithSession:[NSURLSession sharedSession]
                                             authParametersGenerator:^NSString * {
                                                 return [QCOMarvelAuthentication URLParameters];
                                             }];
    QCOFetchCharactersRequestModel *requestModel =
            [[QCOFetchCharactersRequestModel alloc] initWithNamePrefix:@"Spider" pageSize:1 offset:0];
    [service fetchCharactersWithRequestModel:requestModel];
}
@end

Note that I need to provide the Service’s initializer with real arguments. I pass a real NSURLSession, and a block that calls the real QCOMarvelAuthentication. The request itself is for any character whose name starts with “Spider”. I figure the Marvel database will always return something for that! And I limit the page size to 1 result, to make things faster.

Now I fire up the app, and… I see results! I can even turn off my Wi-Fi and see error results.

(Because I use a separate app delegate for tests, this hack can remain in place for a while without affecting unit test runs.)

Spike Tests Are Good for Iteration Demos

At the end of every iteration, the team should give a demo. Some teams struggle with this—they think features need to look like a product. So they skip the demo portion, and end up hunkering down for too long. The product owner is left wondering what progress has been made. (Or worse, is given a meaningless estimate like, “We’re 70% done.”)

Don’t skip the demo. Show the team’s progress, using any way that works.

I suppose some product owners might be put off by seeing NSLog results. But you can explain that a spike test shows actual results with the help of temporary code. You will gain greater trust by having regular demos. And the team will have a better sense of the actual progress.

When You’re Done, Delete the Spike Test

One way or another, when you’re done with the spike test, throw it away.

For a spike solution, I like to work in a branch to keep them from going into production code. The branch can be kept for reference, but it won’t mess up the main branch.

But for a spike test, it depends. You might keep it in a branch, and do the demo straight from your computer. But if you want to demo on someone else’s device, you might check your spike test straight into the master branch for your continuous delivery system. If you go this route, be sure to revert it later.

Summary

A “spike test” is a hybrid of a spike solution and an acceptance test. I doubt that anyone else calls it that, but I needed to call it something. The term may be new, but the idea is old.

Like a spike solution, a spike test is quick & dirty. The goal is fast feedback. Remember to delete it somehow.

A spike test lets you know if things are working so far, before the code is really done. Use it to validate your work in progress. You might also use it for an iteration demo—especially if you’re stuck wondering, “What do we show?” It may not be pretty, but you can show something!

Have you used spike tests for validation? How about for demos? Leave a comment below.

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

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!

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