Positive-Negative Assertions in Cucumber

AuthorMáximo Mussini
·2 min read

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 😃