Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

RSpec retry throw exception and then return value

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.

like image 540
Matilda Avatar asked Jan 08 '13 00:01

Matilda


3 Answers

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?

like image 185
Chris Salzberg Avatar answered Nov 09 '22 04:11

Chris Salzberg


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

like image 44
dolzenko Avatar answered Nov 09 '22 04:11

dolzenko


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
like image 26
xmjw Avatar answered Nov 09 '22 05:11

xmjw