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.