Cucumber steps with variable-length lists

A problem

I’m working hard to learn both RSpec and Cucumber. The verdict is out. I like the idea of Cucumber for happy path behavior testing and RSpec for unit and functional testing. The reality with Cucumber though is that I find I spend a lot of my time constructing regular expressions and pretty abstractions. Here’s a great example of when I start to feel like I’m headed down the wrong path: let’s say you have a feature that could expect a variable number of parameters.

1
2
3
4
5
6
7
  Scenario: Adding two colors
    Given you add the colors "red" and "green"
    Then you should get "yellow"

  Scenario: Adding three colors
    Given you add the colors "red", "green", and "blue"
    Then you should get "white"

Cucumber recommends the matchers:

1
2
3
4
5
6
7
Given(/^you add the colors? "(.*?)" and "(.*?)"$/) do |arg1, arg2, arg3|
  pending
end

Given(/^you add the colors? "(.*?)", "(.*?)", and "(.*?)"$/) do |arg1, arg2, arg3|
  pending
end

This is inconvenient. They’re basically the same. And what if you have four colors? Another matcher? I suspect this is a Cucumber anti-pattern I’m trying to solve, but I was curious how to clean it up. We could to write a regex that handles a variable number of captures, but to the best of my knowledge—apart from maybe some recursive extension to regexes—the “regular” in regex means regular expressions just don’t work that way.

That’s vague and confusing. Consider /"([^"]*)"(?:,\s+)?/. This captures a quoted string perhaps followed by a comma and a space. (Remember that (?:) is a non-capturing group.) Our three-color string has three matches and returns “red”, “green”, and “blue”. Except Cucumber isn’t interested in multiple matches. It wants one match with multiple captures. That’s what regexes won’t do.

A good-enough solution

The obvious alternative is to enumerate some number of step definitions like Cucumber suggested in the first place, but that’s crazytown. But maybe it’s not so crazy if we can roll it up a little:

1
2
3
4
5
(1..10).each do |n|
  Given(%r{^you add the colors? #{Array.new(n,'"([^\"]*)"').join(',?\s+(?:and\s+)?')}$}) do |*args|
    @colors = args
  end
end

This creates ten step definitions, and our two strings match the two they need. Great! But in addition to feeling ugly, it now actually is ugly. Let’s wrap this up in a function. I’ll add this to my support code:

1
2
3
4
5
def for_list_of(pattern, range, &block)
  range.each do |n|
    block.call( Array.new(n,pattern).join(',?\s+(?:and\s+)?') )
  end
end

Then I can write:

1
2
3
4
5
6
7
QUOTED_STRING = /"([^"]*)"/

for_list_of(QUOTED_STRING, 1..10) do |re|
  Given(/^you add the colors? #{re}$/) do |*args|
    @colors = args
  end
end

This works, it’s concise and readable, and it matches the plain English variations:

  • you add the color "red"
  • you add the colors "red" and "green"
  • you add the colors "red", "green", and "blue"
  • you add the colors "red", "green", "blue", and "fuchsia"

This accomplishes what I was trying to do in the first place, so I’m going to consider it a success. Do you know some regex magic I don’t? I’d love to hear about it. Is this an over-beautified Cucumber anti-pattern? Yeah. That’s my suspicion.