Cucumber Tip: Key-Value Tables

You may not realize this: Tables in Cucumber steps don’t have to have a header row. Sometimes it can work really well to use a headerless table of key-value pairs.

Let’s look at an example. Suppose we have a scenario that fills out an advanced search form to search for medical providers matching certain criteria. A mockup of the form looks something like this:

If we were using the recently deprecated web steps generated by cucumber-rails, we might write steps to perform a search like these:


Given I'm on the advanced search page
And I select "Endocrinology" from "Specialty"
And I choose "Yes" within "Accepts Insurance"
And I fill in "ZIP Code" with "90010"
And I select "5 miles" from "Search Radius"
When I press "Search"

But we know better than to do that, right? After all, we’re trying to describe how the search logic should work, not how the form should look. So we try to write our own steps to do these same thing without talking about the implementation so much, but it’s really hard to get away from the implementation. Even if we drive out words like select, fill in, and click, we still have a step per form element.

Suppose we try to combine them into a single step for the domain concept of performing a search. We might get something like:


When I search for an endocrinologist within 5 miles of 90010 who accepts insurance

The implementation details are gone, but it’s long and hard to read. Enter the key-value table:


When I search for a provider with the criteria:
| Provider Type | Doctor |
| Specialty | Endocrinology |
| Accepts Insurance | Yes |
| ZIP | 90010 |
| Search Radius | 5 miles |

In the step definition, we can use the rows_hash method on the table object to convert that key-value table to a single hash. There’s no need to iterate through the rows ourselves. For example, assuming a helper method called provider_search that actually performs the search,


When /^I search for a provider with the criteria:$/ do |table|
criteria = table.rows_hash
provider_search :provider_type => criteria['Provider Type'],
:specialty => criteria['Specialty'],
:accepts_insurance => criteria['Accepts Insurance'].to_bool,
:zip_code => criteria['ZIP'],
:radius => criteria['Search Radius'].to_i
end

You may have noticed the call to to_bool. This uses a helper method like the following to allow us to use yes and no instead of true and false to make more readable scenarios. Put this somewhere in the support directory.


class String
def to_bool
!!(self =~ /^yes|y|true|t|1$/i)
end
end

The call to to_i on the search radius takes advantage of Ruby’s string to integer conversion behavior—if a string starts with an integer but has other text after the integer, the other text is stripped off during the conversion. Try it: Fire up irb (the interactive Ruby console) in a command window and run "5 miles".to_i.

These key-value tables can get long for a complex entity. Not all the details will matter each time. We can make the step read better by introducing a concept of a default provider search. Maybe we have to supply a ZIP code and search radius, but the scenario we’re working on is really about specialties and insurance. We could say,


When I search for a provider with the default criteria and:
| Provider Type | Doctor |
| Specialty | Endocrinology |
| Accepts Insurance | Yes |

In the step definition, we’ll declare the default values in a hash and merge it with the supplied values.


When /^I search for a provider with the default criteria and:$/ do |table|
default_criteria = {
'Provider Type' => 'Doctor',
'Specialty' => 'General',
'Accepts Insurance' => 'Yes',
'ZIP' => 90010,
'Search Radius' => '5 miles'
}

criteria = default_criteria.merge(table.rows_hash)
provider_search :provider_type => criteria['Provider Type'],
:specialty => criteria['Specialty'],
:accepts_insurance => criteria['Accepts Insurance'].to_bool,
:zip_code => criteria['ZIP'],
:radius => criteria['Search Radius'].to_i
end

If the defaults are well known, this is enough. If they need to be documented, one way I like to do it is as executable documentation with a scenario like this:


Scenario: Default Search Criteria
When I search for a provider with the default criteria
Then the search criteria should include:
| Provider Type | Doctor |
| Specialty | General |
| Accepts Insurance | Yes |
| ZIP | 90010 |
| Search Radius | 5 miles |

If you do this, you’ll want to move the default criteria hash into a helper method or constant to make sure it’s the same hash used in the real scenario and the documentation scenario.

We could make the step definition read better by separating the key and value conversions from the use of the data. Cucumber’s table object has map_headers! and map_column!. Unfortunately, those don’t work for rows_hash at this time. I recently submitted a pull request to make it work. Hopefully, we’ll see that in a Cucumber release soon.

Related Articles

Responses