I’ve recently journeyed back into the world of Rails development after an extended stay in more functional languages. One aspect of Ruby that always delights me is its focus on readability. With this focus, along with the ability to fold certain structures, I set out to improve a wordy test.
The original test looked like this:
select_multiple_from "Which of these apply to you?", [
"I read the New York Times every day",
"I read the Washington Post every day",
]
click_on "Submit"
expect(page).to have_css("dd ul li", count: 2)
expect(page).to have_css("dd ul li", text: "I read the New York Times every day")
expect(page).to have_css("dd ul li", text: "I read the Washington Post every day")
The select_multiple_from
handles the multi-select:
def select_multiple_from(from, options)
options.each do |option|
select option, from: from
end
end
The three assertions felt a bit redundant, but necessary: I wanted to ensure form submission captured only the items chosen (by verifying length of items in the list) and that they showed up correctly.
When I write any Ruby, most especially when I’m writing acceptance tests, I favor focusing on readability and working at a single level of abstraction; I came up with:
expect(page).to have_multiple_choice_responses(
"I read the New York Times every day",
"I read the Washington Post every day"
)
This encodes the notion that these two items, and only these two items, should
show up in the list. While there are multiple assertions we’re covering here,
it’s possible to use a feature in many of RSpec’s matchers to chain assertions:
and
. It was introduced in the RSpec::Matchers::Composable
module in
RSpec 3 that many of the Capybara matchers, including have_css
, have
available.
As a first pass, we could write this matcher as such:
def have_multiple_choice_responses(item1, item2)
have_css("dd ul li", count: 2).
and(have_css("dd ul li", text: item1)).
and(have_css("dd ul li", text: item2))
end
There are a few problems here, the most notable that it’s tightly coupled with
the number of items. Let’s make this more flexible with Ruby’s
Enumerable#inject
:
def have_multiple_choice_responses(*args)
count_matcher = have_css("dd ul li", count: args.length)
args.inject(count_matcher) do |matcher, text|
matcher.and(have_css("dd ul li", text: text))
end
end
Enumerable#inject
is called on our array of items we’re expecting, with a
starting matcher (count_matcher
), and for each iteration, extending that
matcher with and
to layer in an additional assertion. This allows the matcher
to grow and have explicit expectations around the number of items present, as
well as their content. This doesn’t assert order, but would be possible using
each.with_index(1)
:
def have_multiple_choice_responses(*args)
count_matcher = have_css("dd ul li", count: args.length)
args.each.with_index(1).inject(count_matcher) do |matcher, (text, offset)|
matcher.and(have_css("dd ul li:nth-of-type(#{offset})", text: text))
end
end
(Note that the nth-of-type
pseudo-selector starts at an index of 1
instead
of 0
)
With this refactoring, we’ve achieved what I’d consider a readable matcher which hits all the assertions necessary, and hides some of the specific CSS selectors behind a well-named method. Success!