Sometimes I want to write a test about code that raises an error and also rolls back changes or skips them entirely. The way to do this in RSpec isn’t immediately obvious.

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.