RSpec Negated Matchers

Engineering Insights

#
Min Read
Published On
March 13, 2025
Updated On
April 25, 2025
RSpec Negated Matchers

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:

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:

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:

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:

RSpec::Matchers.define_negated_matcher :not_change, :change

Which allows us to then write this:

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:

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.

Author headshot
Written by
, The Gnar Company

Related Insights

See All Articles
Engineering Insights
Anthropic Dropped OpenClaw Support. Here's How I Replaced It With Claude Code.

Anthropic Dropped OpenClaw Support. Here's How I Replaced It With Claude Code.

Anthropic's TOS change killed OpenClaw overnight, taking businesses built on the ecosystem with it. But for end users, Claude Code's new channels feature offers a viable path forward.
Product Insights
We Turned a Phone Call Into a Working Product in 48 Hours. Here's Exactly How.

We Turned a Phone Call Into a Working Product in 48 Hours. Here's Exactly How.

Watch what happens when a one-hour phone call becomes a working application in 48 hours. We walk through exactly how Context-Driven Development turns a single conversation into a competitor analysis, feature prioritization, full PRD, and production-grade software with Stripe billing, user accounts, and an admin dashboard—using AI-assisted agentic development with a human architect in the loop.
News
Is Your Team Ready for AI? Here's How to Find Out in 2 Minutes

Is Your Team Ready for AI? Here's How to Find Out in 2 Minutes

Most teams aren't getting real value from AI tools — not because the tools don't work, but because their foundations aren't ready. Discover the five factors that predict AI success and take a free 2-minute assessment to find out where your team stands.
Previous
Next
See All Articles