Skip to main content
Published in Engineering Insights

RSpec Negated Matchers

RSpec Negated Matchers
Table of contents
  1. Problem
  2. Solution

Problem#

We want to test that when we call our Ruby method, one result does occur while another result does not occur. Specifically, we want to test that an Event is created but that an EventPayload is not created. Using RSpec, we have this so far:

Ruby
it "creates an event without an associated payload" do
  expect { service.create_event! }.to change { Event.count }.by(1)
  expect { service.create_event! }.not_to change { EventPayload.count }
end

This is ok, but we'd like to run both expectations on a single method call.

We know that RSpec has and which can help us make compound expectations, but this falls apart when we want to make one positive assertion and one negative assertion. We could try this:

Ruby
it "creates an event without an associated payload" do
  expect { service.create_event! }
    .not_to change { EventPayload.count }
    .and change { Event.count }.by(1)
end

But no joy:

Ruby
NotImplementedError: expect(...).not_to matcher.and matcher` is not supported, since it creates a bit of an ambiguity.

Solution#

How can we chain two expectations together with and if one is a "not" condition? RSpec allows us to define a negated matcher:

Ruby
RSpec::Matchers.define_negated_matcher :not_change, :change

Which allows us to then write this:

Ruby
it "creates an event without an associated payload" do
  expect { service.create_event! }
    .to change { Event.count }.by(1)
    .and(not_change { EventPayload.count })
end

And we can flip the order if we choose:

Ruby
it "creates an event without an associated payload" do
  expect { service.create_event! }
    .to not_change { EventPayload.count }
    .and change { Event.count }.by(1)
end

Fun!

Learn more about how The Gnar builds Ruby on Rails applications.

Frequently asked questions

Why combine these expectations?

The goal is to test both outcomes from a single call to service.create_event!. Separate expectations run that call twice.

How do I define not_change?

Use RSpec::Matchers.define_negated_matcher :not_change, :change. This defines a negated version of the change matcher.

Can the negative expectation come first?

Yes. The example starts with .to not_change { EventPayload.count } and follows it with .and change { Event.count }.by(1).

Stay In The Loop

Never miss an update. Get the latest blogs, webinars, and expert insights straight to your inbox from The Gnar.