25

I have a retry block

 def my_method
    app_instances = []
    attempts = 0
    begin 
      app_instances = fetch_and_rescan_app_instances(page_n, policy_id, policy_cpath)
    rescue Exception
      attempts += 1
      retry unless attempts > 2
      raise Exception 
    end
    page_n += 1
  end

where fetch_and_rescan_app_instances access the network so can throw an exception.

I want to write an rspec test that it throws an exception first time and doesn't throw an exception second time it gets called, so I can test if the second time it doesn't throw an exception, the my_method won't throw an exeption.

I know i can do stub(:fetch_and_rescan_app_instances).and_return(1,3) and first time it returns 1 and second time 3, but I don't know how to do throw an exception first time and return something second time.

Matilda
  • 1,708
  • 3
  • 25
  • 33

3 Answers3

24

You can calculate the return value in a block:

describe "my_method" do
  before do
    my_instance = ...
    @times_called = 0
    my_instance.stub(:fetch_and_rescan_app_instances).and_return do
      @times_called += 1
      raise Exception if @times_called == 1
    end
  end

  it "raises exception first time method is called" do
    my_instance.my_method().should raise_exception
  end

  it "does not raise an exception the second time method is called" do
    begin
      my_instance.my_method()
    rescue Exception
    end
    my_instance.my_method().should_not raise_exception
  end
end

Note that you should really not be rescuing from Exception, use something more specific. See: Why is it a bad style to `rescue Exception => e` in Ruby?

Community
  • 1
  • 1
Chris Salzberg
  • 27,099
  • 4
  • 75
  • 82
  • Added `my_instance` in there so you get `my_instance.stub(:fetch_and_rescan_app_instances)` etc. Calling `stub(:fetch_and_rescan_app_instances)` just like that will not work. – Chris Salzberg Jan 08 '13 at 02:47
  • Great thanks! @shioyama, I udnerstood why not to do rescue Exception, but what if I don't know what kind of exact exception is it gonna throw? I know it's a network call so it might have connection issues or anything else, but not sure what type of exception it would be. – Matilda Jan 08 '13 at 07:27
  • 1
    @Meena, you could try a regular rescue, which just rescues from StandardError. – Jenn Jan 08 '13 at 20:47
  • 3
    Note that when using `allow` syntax (possibly just rspec3 in general) you omit the `and_return`: `allow(my_instance).to receive(:fetch_and_rescan_app_instances) do...` – d3vkit Feb 07 '16 at 21:17
21

What you do is constrain the times the message should be received (receive counts), i.e. in your case you can

instance.stub(:fetch_and_rescan_app_instances).once.and_raise(RuntimeError, 'fail')
instance.stub(:fetch_and_rescan_app_instances).once.and_return('some return value')

Calling instance.fetch_and_rescan_app_instances first time will raise RuntimeError, and second time will return 'some return value'.

PS. Calling more than that will result in an error, you might consider using different receive count specification https://www.relishapp.com/rspec/rspec-mocks/docs/message-expectations/receive-counts

Ev Dolzhenko
  • 6,100
  • 5
  • 38
  • 30
13

This has changed a little in RSpec3.x. It seems the best approach is to pass a block to the receive that defines this type of behaviour.

The following is from the docs suggesting how to create this type of transit failure:

(This errors every other time it is called... But is easy to adapt.)

RSpec.describe "An HTTP API client" do
  it "can simulate transient network failures" do
    client = double("MyHTTPClient")

    call_count = 0
    allow(client).to receive(:fetch_data) do
      call_count += 1
      call_count.odd? ? raise("timeout") : { :count => 15 }
    end

    expect { client.fetch_data }.to raise_error("timeout")
    expect(client.fetch_data).to eq(:count => 15)
    expect { client.fetch_data }.to raise_error("timeout")
    expect(client.fetch_data).to eq(:count => 15)
  end
end
xmjw
  • 3,154
  • 21
  • 29