Using spikes is a crucial tool in the agile developers arsenal. I’ve noticed that there is often a post-spike depression in a developers speed when they begin to incorporate the concepts from the “throw away” code into the application and they must resume writing tests. Usually when asked why they didn’t test-drive his or her spike a developer will answer “It’s nearly impossible to test code when you have no idea what you’re doing!”.
I’d like to walk you though my approach to spike development that keeps BDD in the loop.
Enter the Learning Test Strategy
The learning test strategy is a pretty simple concept. Instead of just writing code you throw away, you begin by writing acceptance tests for what you’d like to implement during your spike and then write throw away code to satisfy the criteria set forth in the tests. Instead of just having throw away code you now have an integration test suite for the features you are about to develop.
It’s Cucumber, I know this!
The very existence of a spike hints that you have one or more feature stories that implement the systems you are exploring inside your codebase. To me, this sounds an awful lot like acceptance criteria that I can build into executable Cucumber tests during my spike story.
I’ve recently developed a document management library that allows our application to create folders and files on a cloud based document management service. No one on the team had developed against the API before, so we needed to spike against it. We knew that ultimately our system should:
- Create a folder when a new clinic is added to our system
- Create a folder relative to the clinic folder for a patient when they are registered
So now I know what I need to ask from the API. I could just start writing code to discover how the API works, or I could write a Cucumber feature instead:
@slow
Feature: Creating a folder on Box
As a user,
In order to organize documents on Box,
I'd like to be able to create folders
Scenario: Creating a folder in the root directory
Given I have authenticated with Box
When I create a folder named "Walrus Love"
Then I should be able to retrieve the "Walrus Love" folder
Scenario: Create a subfolder on Box
Given I have authenticated with Box
And the "Walrus Love" folder exists
When I create a folder named "Stop clubbing, seals" within "Walrus Love"
Then "Stop clubbing, seals" should be a child folder of "Walrus Love"
Do note the @slow
tag, this indicates that I’m writing an integration test that
interfaces directly with an external library and can take some time and have
network connectivity issues. You can eliminate this by using VCR, which I will
utilize elsewhere in the test suite, but there is something to be said for
having a pure integration test set on a controllable tag. I can skip running
the @slow tag while locally developing, but enforce running the test for a
production build if I so desire.
Now I can feel free to write some terrible inefficient and hacky code in the step definitions to make these scenarios pass:
require 'box-sdk'
Given /^I have authenticated with Box$/ do
@account = Box::Account.new BOX_AUTH_TOKEN, BOX_AUTH_KEY
end
When /^I create a folder named "([^"]*)"$/ do |folder_name|
@account.root.create folder_name
end
Then /^I should be able to retrieve the "([^"]*)" folder$/ do |folder_name|
retrieved_folder = @account.root.at("#{folder_name}/")
retrieved_folder.name.should == folder_name
end
Given /^the "([^"]*)" folder exists$/ do |folder_name|
step %{I create a folder named "#{folder_name}"}
end
When /^I create a folder named "([^"]*)" within "([^"]*)"$/ do |subfolder_name, folder_name|
parent_folder = @account.root.at("#{folder_name}/")
parent_folder.create subfolder_name
end
Then /^"([^"]*)" should be a child folder of "([^"]*)"$/ do |subfolder_name, folder_name|
child_folder = @account.root.at("#{folder_name}/#{subfolder_name}/")
child_folder.name.should == subfolder_name
end
The tests aren’t DRY, the code isn’t DRY, but now I have an understanding of the API I’m implementing. I can already start to see pain points in the API implementation, and in future stories I can write a more effective wrapper to alleviate them.
Refactoring Your Learning Tests During Feature Development
Once I begin feature development, I need to ensure I clean up my learning tests as I implement a wrapper for the API. This ensures I have a fully functional suite of intergration tests so that I can judiciously mock, stub, and VCR my unit tests where the application is concerned.
Here’s something closer to the actual result:
When /^I create a folder named "([^"]*)"$/ do |folder_name|
DocumentManager::Folder.create folder_name
end
Then /^I should be able to retrieve the "([^"]*)" folder$/ do |folder_name|
retrieved_folder = DocumentManager::Folder.find_by_name folder_name
retrieved_folder.name.should == folder_name
end
Given /^the "([^"]*)" folder exists$/ do |folder_name|
step %{I create a folder named "#{folder_name}"}
end
When /^I create a folder named "([^"]*)" within "([^"]*)"$/ do |subfolder_name, folder_name|
DocumentManager::Folder.create(subfolder_name, folder_name)
end
Then /^"([^"]*)" should be a child folder of "([^"]*)"$/ do |subfolder_name, folder_name|
child_folder = DocumentManager::Folder.find_by_name subfolder_name
child_folder.parent.should == DocumentManager::Folder.find_by_name folder_name
end
Wrapping it up
It’s often easy to forget that Cucumber is more than just testing how a user will interact with the UI of an application. It can really be a powerful tool to keep you focused on learning exactly what you need when you are in uncharted coding waters. Furthermore, it turns an exercise where you’d typically throw away code into an exercise when you walk away with a basic framework for testing future features. I’ve found this to be extremely valuable when exploring third party API’s and new development techniques.