RSpec: TypeError When Stubbing Custom Exception

Viewed 801

I'd like to test that Custom::Runner.run rescues all StandardErrors and fires an alert with the exception.

I'm having some trouble figuring out how to stub the call to my custom error class, Custom::Error, and expecting that Custom::Alert.error was received with the double as an argument.

Here's a complete test case to demonstrate the issue:

module Custom
  class Error < StandardError
  end
end

module Custom
  class Alert
    def self.error(exception, context = {})
    end
  end
end

module Custom
  class Runner
    def self.run
      request
    rescue => e
      Custom::Alert.error(e, { foo: :bar })
    end

    class << self
      def request
        raise Custom::Error.new('test')
      end
    end
  end
end

Here's the test:

RSpec.describe Custom::Runner do
  describe '.run' do
    let(:custom_error_double) { instance_double(Custom::Error) }

    before do
      # could this be the culprit?
      allow(Custom::Error).to receive(:new)
        .with('test')
        .and_return(custom_error_double, 'test')
    end

    it 'fires a custom alert' do
      expect(Custom::Alert).to receive(:error)
        .with(custom_error_double, foo: :bar)

      described_class.run
    end
  end
end

The test fails:

Failures:                                                                                                                                                    
                                                                                                                                                                1) Custom::Runner.run fires a custom alert
     Failure/Error: Custom::Alert.error(e, { foo: :bar })                                                                                                    

       #<Custom::Alert (class)> received :error with unexpected arguments                                                                                    
         expected: (#<InstanceDouble(Custom::Error) (anonymous)>, {:foo=>:bar})                                                                              
              got: (#<TypeError: exception class/object expected>, {:foo=>:bar})
       Diff:             
       @@ -1,2 +1,2 @@
       -[#<InstanceDouble(Custom::Error) (anonymous)>, {:foo=>:bar}]
       +[#<TypeError: exception class/object expected>, {:foo=>:bar}]

I believe this is because rescue requires an exception and I'm returning a double. I've tried raising .and_raise(custom_error_double), but I continue to get the same TypeError: exception class/object expected.

There must be something I'm missing here. Any advice would be appreciated.

2 Answers

I guess the instance double of Custom::Error is an InstanceDouble object and not an Exception object so when you raise the double it causes the TypeError.

You could replace the double

 let(:custom_error_double) { instance_double(Custom::Error) }

with a real Custom::Error object

 let(:custom_error) { Custom::Error.new }

to avoid that.

I believe that you have it exactly right that the exception vs. the double is the issue. The error specifically is received :error with unexpected arguments, and the comparison is that the double does not match the TypeError. In this case, the blind rescue => e then calls Custom::Alert.error(e, {foo: :bar}) (which has the TypeError as the argument e), but in your test the .with() is expecting the double.

This will work:

RSpec.describe Custom::Runner do
  describe '.run' do
    let(:custom_error) { Custom::Error.new }

    before do
      allow(Custom::Error).to receive(:new).with('test').and_return(custom_error, 'test')
    end

    it 'fires a custom alert' do
      expect(Custom::Alert).to receive(:error).with(custom_error, foo: :bar)

      described_class.run
    end
  end
end
Related