Cucumber steps often involve asserting a condition or the opposite of it. For example:
Then(/^I should see a "(.*?)" comment$/) do |message|
expect(page).to have_css('li.comment', text: message)
end
Then(/^I should not see a "(.*?)" comment$/) do |message|
expect(page).not_to have_css('li.comment', text: message)
end
Not only is it boring to write steps like this: it also has the downside of introducing duplication into our test steps. If the DOM changes, we are forced to update both steps.
Even worse, if the DOM changes and we forget to update the negative assertion, it will always pass since the element doesn't even exist!
Let's give it another shot, but this time we will encapsulate the DOM references:
def have_comment(message)
have_css('li.comment', text: message)
end
Then(/^I should see a "(.*?)" comment$/) do |message|
expect(page).to have_comment(message)
end
Then(/^I should not see a "(.*?)" comment$/) do |message|
expect(page).not_to have_comment(message)
end
The code is easier to read, and we solved a potential maintainability issue, nice! It's possible to add a little touch of regular expressions to combine both steps into one:
Then(/^I should( not)? see a "(.*?)" comment$/) do |should_not, message|
if should_not
expect(page).not_to have_comment(message)
else
expect(page).to have_comment(message)
end
end
If we write "should not see" then the group will match and the should_not
variable will contain " not"
. If we write "should see" then the group won't capture anything and should_not
will be nil
. This allows us to make a simple conditional check.
We can leverage Ruby's expressiveness and take it a bit further:
Then(/^I should( not)? see a "(.*?)" comment$/) do |should_not, message|
expect(page).send (should_not ? :not_to : :to), have_comment(message)
end
Shorter, but not necessarily easier to understand. Fortunately, we can create our own custom RSpec matcher to encapsulate this pattern and make it easier to reuse and understand:
# features/support/to_or.rb
module PositiveNegativeExpectationHandler
def to_or(not_to, matcher, message=nil, &block)
if not_to
not_to(matcher, message, &block)
else
to(matcher, message, &block)
end
end
end
RSpec::Expectations::ExpectationTarget.send(:include, PositiveNegativeExpectationHandler)
Finally, we are able to express the step in a very concise and readable way:
Then(/^I should( not)? see a "(.*?)" comment$/) do |not_to, message|
expect(page).to_or not_to, have_comment(message)
end
No magic here 🎩, just clever naming 🐰
to_or
has been a very helpful addition to our projects, allowing us to write simple steps that read like English.
You can copy the PositiveNegativeExpectationHandler
to a support file in your project, like features/support/to_or.rb
, and start using it right away 😃