I saw an error that can occur when you press a button too many times and I dealt with it with a rescue, so the controller is:
def all_list
filename = "#{current_organization.name.gsub(' ', '_')}.zip"
temp_file = Tempfile.new(filename)
begin
#things to create a zip
rescue Errno::ENOENT
puts 'Something went wrong'
redirect_to all_list_report_path
ensure
temp_file.close
temp_file.unlink
end
end
And I tried a lot of things with rspec to test this rescue block, my last try was:
context 'with a logged user that try to' do
before do
login(member1.user)
allow(ReportsController).to receive(:all_list).and_raise(Errno::ENOENT)
end
it 'throws the error but downloads the zip' do
get :all_list
expect(ReportsController).to receive(:all_list).and_raise(Errno::ENOENT)
end
end
It seems to work, but the block rescue is not covered and I tried to see if the block is called with puts 'Something went wrong' but obviusly does not print anything.
I'm looking for a way to cover the block effectively. Any help is welcome.
You have to raise the error for method which are called inside the begin block. Raising error for all_list method will never execute begin and rescue block.
def all_list
filename = "#{current_organization.name.gsub(' ', '_')}.zip"
temp_file = Tempfile.new(filename)
begin
things_to_create_a_zip
rescue Errno::ENOENT
puts 'Something went wrong'
redirect_to all_list_report_path
ensure
temp_file.close
temp_file.unlink
end
end
Allowing things_to_create_a_zip method to raise the error Errno::ENOENT, will executes the rescue block.
context 'with a logged user that try to' do
before do
login(member1.user)
allow(subject).to receive(:things_to_create_a_zip).and_raise(Errno::ENOENT)
end
it 'redirect to all_list_report_path' do
get :all_list
expect(response).to redirect_to(all_list_report_path)
end
end
Related
I am writing a test which should verify that when the object creation saves, indeed the exception is caught.
begin
object.create!(item)
rescue => exception
exception.message
end
rspec
expect{ exception.message }.to eq("Validation failed: Item name can't be blank")
output I got:
Failure/Error: expect{ exception.message }.to eq("Validation failed: Item name can't be blank")
You must pass an argument rather than a block to `expect` to use the provided matcher (eq "Validation failed: Item name can't be blank"), or the matcher must implement `supports_block_expectations?`.
Can anyone help me out?
You cannot test your code like that because the exception variable only exists in the rescue block. Instead, you need to call the method you want to test and add an expectation of what it should return.
Your example actually doesn't have such a method that could be called. I imagine that your method actually looks like this:
def do_stuff_with(object, item)
begin
object.create!(item)
rescue => exception
exception.message
end
end
Then you should be able to have an expectation like this:
expect(do_stuff_with(object, item)).to eq(
"Validation failed: Item name can't be blank"
)
I'm doing a stub request to File but since I call Tempfile (that is a subclass of File) before calling File, Tempfile is intercepting the stub I define.
Model:
def download_file
#...
begin
tempfile = Tempfile.new([upload_file_name, '.csv'])
File.open(tempfile, 'wb') { |f| f.write(result.body.to_s) }
tempfile.path
rescue Errno::ENOENT => e
puts "Error writing to file: #{e.message}"
e
end
end
Rspec example:
it 'could not write to tempfile, so the report is created but without content' do
allow(File).to receive(:open).and_return Errno::ENOENT
response_code = post #url, params: { file_ids: [#file.id] }
expect(response_code).to eql(200)
assert_requested :get, /#{#s3_domain}.*/, body: #body, times: 1
report = #file.frictionless_report
expect(report.report).to eq(nil)
end
Error:
tempfile in the line
tempfile = Tempfile.new([upload_file_name, '.csv'])
receives Errno::ENOENT, indicating that the stub is going to Tempfile instead of to File.
How can I define the stub to go to File instead of to Tempfile?
There's no need to reopen a Tempfile, it's already open and delegates to File.
def download_file
tempfile = Tempfile.new([upload_file_name, '.csv'])
tempfile.write(result.body.to_s)
tempfile.path
# A method has an implicit begin.
rescue Errno::ENOENT => e
puts "Error writing to file: #{e.message}"
e
end
Then you can mock just Tempfile.new. Note that exceptions are raised, not returned.
it 'could not write to tempfile, so the report is created but without content' do
# Exceptions are raised, not returned.
allow(Tempfile).to receive(:new)
.and_raise Errno::ENOENT
response_code = post #url, params: { file_ids: [#file.id] }
expect(response_code).to eql(200)
assert_requested :get, /#{#s3_domain}.*/, body: #body, times: 1
report = #file.frictionless_report
expect(report.report).to eq(nil)
end
However, this remains fragile glass-box testing. Your test has knowledge of the implementation, if the implementation changes the test gives a false negative. And it still has to hope mocking Tempfile.new doesn't break something else.
Instead, extract temp file creation from download_file.
private def new_temp_file_for_upload
Tempfile.new([upload_file_name, '.csv'])
end
def download_file
tempfile = new_temp_file_for_upload
tempfile.write(result.body.to_s)
tempfile.path
rescue Errno::ENOENT => e
puts "Error writing to file: #{e.message}"
e
end
Now the mocking can be targeted to a specific method in a specific object. And we can apply some good rspec patterns.
context 'when the Tempfile cannot be created' do
# Here I'm assuming download_file is part of the Controller being tested.
before do
allow(#controller).to receive(:new_temp_file_for_upload)
.and_raise Errno::ENOENT
end
it 'creates the report without content' do
post #url, params: { file_ids: [#file.id] }
expect(response).to have_http_status(:success)
assert_requested :get, /#{#s3_domain}.*/, body: #body, times: 1
report = #file.frictionless_report
expect(report.report).to be nil
end
end
Note: returning "success" and an empty report after an internal failure is probably incorrect. It should return a 5xx error so the user knows there was a failure without having to look at the content.
download_file is doing too many things. It's both downloading a file and deciding what to do with a specific error. It should just download the file. Let something higher up in the call stack decide what to do with the exception. Methods which do one thing are simpler and more flexible and easier to test and less buggy.
private def new_temp_file_for_upload
Tempfile.new([upload_file_name, '.csv'])
end
def download_file
tempfile = new_temp_file_for_upload
tempfile.write(result.body.to_s)
tempfile.path
end
context 'when the download fails' do
before do
allow(#controller).to receive(:download_file)
.and_raise "krunch!"
end
it 'responds with an error' do
post #url, params: { file_ids: [#file.id] }
expect(response).to have_http_status(:error)
end
end
Note that no specific error is needed. It's enough that download_file raises an exception. This test now has no knowledge of the internals beyond knowing that download_file is called.
Here's my begin..rescue..ensure block. I want to write some test cases that after error is raised, the final result {} will be returned.
I am using rspec 3.3.
def external_call
result = ExternalApi.call
rescue => e
# handle the error, and re-raise
Handler.handle(e)
raise
ensure
result.presence || {}
end
I have wrote test case for the rescue part:
context 'when external api raise error' do
it 'handles the error, and re-raise' do
allow(ExternalApi).to receive(:call).and_raise(SomeError)
expect(Handler).to receive(:handle).with(e)
expect { subject.external_call }.to raise_error(SomeError)
end
end
But I am not sure how to test the ensure part after the error is re-raised.
Here's my attempt:
it 'returns {} after error raised' do
allow(ExternalApi).to receive(:call).and_raise(SomeError)
result = subject.external_call
expect(result).to eq({})
end
In this case, the test case will fail in the subject.external_call line, since it will raise error there. I am not sure how to test this cases after the error is re-raised.
When using begin/rescue/ensure block with implicit returns, ruby will return the last method to be run in the rescue block as the return value, not the ensure. If the value from the ensure block needs to be returned, it will either have to be explicitly returned, or not included in an ensure but instead moved outside of the begin/rescue block.
Below is an example which shows the difference.
class TestClass
def self.method1
raise 'an error'
rescue
'rescue block'
ensure
'ensure block'
end
def self.method2
raise 'an error'
rescue
'rescue block'
ensure
return 'ensure block'
end
def self.method3
begin
raise 'an error'
rescue
'rescue block'
end
'ensure equivalent block'
end
end
RSpec.describe TestClass do
it do
# does not work, method1 returns 'rescue block'
expect(TestClass.method1).to eql 'ensure block'
end
it do
# does work, as method2 explicitly returns 'ensure block'
expect(TestClass.method2).to eql 'ensure block'
end
it do
# does work, as method3 uses 'ensure equivalent block' as the inferred return
expect(TestClass.method3).to eql 'ensure equivalent block'
end
end
Help me make this test pass:
Here is an example of some rspec code,
class User
attr_accessor :count
def initialize
#count = 0
end
# sometimes raises
def danger
puts "IO can be dangerous..."
rescue IOError => e
#count += 1
end
#always raises
def danger!
raise IOError.new
rescue IOError => e
#count += 1
end
end
describe User do
describe "#danger!" do
it "its rescue block always increases the counter by one" do
allow(subject).to receive(:'danger!')
expect {
subject.danger!
}.to change(subject, :count).by(1)
end
end
describe "#danger" do
context "when it rescues an exception" do
it "should increase the counter" do
allow(subject).to receive(:danger).and_raise(IOError)
expect {
subject.danger
}.to change(subject, :count).by(1)
end
end
end
end
I've also created a fiddle with these tests in it, so you can just make them pass. Please help me test the rescue block of a method!
Background:
My original question went something like this:
I have a method, like the following:
def publish!(resource)
published_resource = resource.publish!(current_project)
resource.update(published: true)
if resource.has_comments?
content = render_to_string partial: "#{ resource.class.name.tableize }/comment", locals: { comment: resource.comment_content_attributes }
resource.publish_comments!(current_project, published_resource.id, content)
end
true
rescue Bcx::ResponseError => e
resource.errors.add(:base, e.errors)
raise e
end
And I want to test that resource.errors.add(:base, e.errors) is, in fact, adding an error to the resource. More generally, I want to test the rescue block in a method.
So I'd like to write code like,
it "collects errors" do
expect{
subject.publish!(training_event.basecamp_calendar_event)
}.to change(training_event.errors.messages, :count).by(1)
end
Of course, this raises an error because I am re-raising in the rescue block.
I've seen a few answers that use the old something.stub(:method_name).and_raise(SomeException), but rspec complains that this syntax is deprecated. I would like to use Rspec Mocks 3.3 and the allow syntax, but I'm having a hard time.
allow(something).to receive(:method_name).and_raise(SomeException)
would be the new allow syntax. Check out the docs for reference.
I was misunderstanding what the allow syntax is actually for. So to make my example specs pass, I needed to do this:
describe "#danger" do
context "when it rescues an exception" do
it "should increase the counter" do
allow($stdout).to receive(:puts).and_raise(IOError) # <----- here
expect {
subject.danger
}.to change(subject, :count).by(1)
end
end
end
This thing that I'm stubing is not the method, or the subject, but the object that might raise. In this case I stub $stdout so that puts will raise.
Here is another fiddle in which the specs are passing.
Hey,
I am trying to use Mocha and Rspec to test a scenario where a method always raises some exception.
Here is the controller code I am trying to test:
def add_parent
begin
parent = Entity.find_by_id(params[:parent_id])
if !parent.nil?
#entity.add_parent!(parent)
flash[:success] = "Entity successfully updated."
else
raise "Parent does not exist."
end
rescue
flash[:error] = "Something bad happened. #{$!}"
end
redirect_to #entity
end
Here is the test code:
it "should flash error if exception is thrown when adding parent" do
Entity.any_instance.stubs(:add_parent!).raises(Exception)
lambda do
post :add_parent, :id => #entity[:id],
:parent_id => #parent_entity[:id]
end.should_not change(#entity.parents, :count)
flash[:error].should =~ /something bad happened/i
end
Here is the method which is being stubbed:
def add_parent!(parent)
Entity.transaction do
lock!('lock in share mode')
self.parents << parent
end
end
I am getting the following rspec error, which is pretty uninformative so I don't know how to resolve it..
Failures:
1) EntitiesController POST 'add_parent' for signed-in users allow access with edit permission should flash error if exception is thrown when adding parent
Failure/Error: post :add_parent, :id => #entity[:id],
Exception
# ./app/controllers/entities_controller.rb:81:in `add_parent'
# ./spec/controllers/entities_controller_spec.rb:1010
Wooah, first of all, it's a very bad habit to rescue from a big chunk of code without give the exception class you are expecting here. rescueing everything is no good. Best remove the whole rescueing, maybe use find over find_by_id so rails can catch that 404 error and you aren't bothered about. On the other hand, is this error supposed to happen a lot? Looks like some db-stuff going on so I wouldn't expect it to fail.
Secondly, I think you must test like the manual says about raise_error
http://relishapp.com/rspec/rspec-expectations/v/2-6/dir/built-in-matchers/raise-error-matcher
rescue with no argument will rescue StandardError and its descendants. If you want to catch ALL exceptions, you should rescue Exception as all exceptions descend from class Exception.
Source: http://www.ruby-forum.com/topic/44495