# Custom negated matchers for RSpec

Posted on August 31, 2017 by Robin Neumann

Keeping RSpec examples as atomic and lean as possible is a good rule of thumb. Nevertheless, sometimes it makes sense to combine multiple expectations into one test case - for example when the evaluation of the expression you want to test is “expensive” in some sense.

Let us study the Ruby class Pirate and add some specs for it:

class Pirate
attr_accessor :mood

def initialize
@mood = 10
end

def insult(other_pirate)
self.mood += 1
other_pirate.mood -= 1
end
end


When a pirate instance is insulting another pirate, this is affecting the fighter’s moods:

guybrush, le_chuck = Pirate.new, Pirate.new

guybrush.mood # => 10
le_chuck.mood # => 10

guybrush.insult(le_chuck)

guybrush.mood # => 11
le_chuck.mood # => 9


Let us turn this into proper test cases with RSpec. One way of expressing this is writing down two seperate test cases:

RSpec.describe Pirate do
let(:guybrush) { Pirate.new }
let(:le_chuck) { Pirate.new }

it { expect { guybrush.insult(le_chuck) }.to change { le_chuck.mood }.by(-1) }
it { expect { guybrush.insult(le_chuck) }.to change { guybrush.mood }.by(1) }
end


But now let’s assume we really want to combine this into a single example where the expression guybrush.insult(le_chuck) is only evaluated once. One way to combine the expectations is wrapping one test into another, like the composition of functions in mathematics:

RSpec.describe Pirate do
let(:guybrush) { Pirate.new }
let(:le_chuck) { Pirate.new }

it "affects the fighting pirate's moods" do
expect {
expect { guybrush.insult(le_chuck) }.to change { le_chuck.mood }.by(-1)
}.to change { guybrush.mood }.by(1)
end
end


Unfortunately the probability is high that this strategy results in messy and unreadable test code as soon as you combine 3 or more expectations.

Better (imho) is using the neat method and that RSpec provides:

RSpec.describe Pirate do
let(:guybrush) { Pirate.new }
let(:le_chuck) { Pirate.new }

it "affects the fighting pirate's moods" do
expect { guybrush.insult le_chuck }
.to change { le_chuck.mood }.by(-1)
.and change { guybrush.mood }.by(1)
end
end


This is really elegant, but problems arise once you want to include tests for the fact that the corresponding expression guybrush.insult(le_chuck) does not affect some other object.

This is a priori not possible with the plain and-method above. But again RSpec has a really nice built-in solution for this problem: Definition of custom negated matchers! It’s pretty simple:

# This could live in your spec_helper.rb or wherever you configure RSpec
RSpec::Matchers.define_negated_matcher :not_change, :change


Afterwards you can happily use the operator not_change, the negated operator to change as we have defined above:

RSpec.describe Pirate do
let(:guybrush) { Pirate.new }
let(:le_chuck) { Pirate.new }
let(:otis)     { Pirate.new }
let(:carla)    { Pirate.new }

it "affects the fighting pirate's moods" do
expect { guybrush.insult le_chuck }
.to change { le_chuck.mood }.by(-1)
.and change { guybrush.mood }.by(1)
.and not_change { otis.mood }
.and not_change { carla.mood }
end
end


Give credit where credit is due: I found this solution within a StackOverflow answer. Thanks to the author!