Sometimes you need to write a test for code that rolls back changes and re-raises an error, or a
test that refutes a critical side-effect even when an error is raised. The way to do this in
RSpec isn’t immediately obvious. It’s possible to chain assertions together using and
, so this
might be what you write.
it "fails without side-effects" do
values = [1,2,3]
expect {
raise "Kaboom"
values.pop
}.to raise_error("Kaboom")
.and_not change { values }
end
This isn’t supported in RSpec because there is no built-in way to invert the and
method.
You could write your own custom matcher to express this, but I think the simplest way is to
write a nested expectation.
it "fails without side-effects" do
values = [1,2,3]
expect {
expect {
raise "Kaboom"
values.pop
}.to raise_error("Kaboom")
}.to_not change { values }
end
It might be a little surprising that RSpec supports this. It works because the inner expect
rescues the exception. If the exception doesn’t match or isn’t raised, then it raises an
ExpectationNotMetError
, which the outer expect passes through, causing the test to fail. What
if the blocks are nested the other way?
it "fails without side-effects" do
values = [1,2,3]
expect {
expect {
values.pop
raise "Kaboom"
}.to_not change { values }
}.to raise_error("Kaboom")
# Backup assertion...
expect(users).to eq ["Bob"]
end
In this case, the test yields a false positive. This happens because the inner expect
does not
handle the exception, so the to_not change
assertion isn’t executed. Meanwhile the outer
expect
passes because it expects an exception to be raised. As a result, this test will pass
even if the unwanted side-effects occur.