ActionDispatch::IntegrationTest suppresses exceptions - ruby-on-rails

When debugging failing integration tests, I keep running into the same problem where the exceptions raised in my code are suppressed and not shown in the testing output.
For example, for the following controller and test:
class RegistrationController::ApplicationController
def create
# some code that raises an exception
end
end
class RegistrationFlowTest < ActionDispatch::IntegrationTest
test 'user registers successfully' do
post sign_up_path, params: { username: 'arnold', password: '123' }
assert_response :success
end
end
The output is something like
Minitest::Assertion: Expected response to be a <2XX: success>, but was a <500: Internal Server Error>
Is there a way to see the exact raised exception? Instead of just the difference of HTTP response code?
Thanks!
Simon

My recommended approach to this issue would be to actually parse the response provided by Rails (at least by default in test and development environments) which includes the stacktrace for the error and handle that in the case that your test fails. This has the advantage that it won't output the stacktrace when errors are raised that don't result in failing tests (e.g. scenarios where you are intentionally testing how failures are handled).
This little module that I've crafted will allow you to call assert_response_with_errors to assert the response to a call but output the exception message and stack trace in a readable format when the response is not what you expected.
module ActionDispatch
module Assertions
module CustomResponseAssertions
# Use this method when you want to assert a response body but also print the exception
# and full stack trace to the test console.
# Useful when you are getting errors in integration tests but don't know what they are.
#
# Example:
# user_session.post create_gene_path, params: {...}
# user_session.assert_response_with_errors :created
def assert_response_with_errors(type, message = nil)
assert_response(type, message)
rescue Minitest::Assertion => e
message = e.message
message += "\nException message: #{#response.parsed_body[:exception]}"
stack_trace = #response.parsed_body[:traces][:'Application Trace'].map { |line| line[:trace] }.join "\n"
message += "\nException stack trace start"
message += "\n#{stack_trace}"
message += "\nException stack trace end"
raise Minitest::Assertion, message
end
end
end
end
To use this, you need to include it into ActionDispatch::Assertions before Rails has loaded its stack in your test_helper.rb. So just prepend the include into your test_helper.rb, like this:
ActionDispatch::Assertions.include ActionDispatch::Assertions::CustomResponseAssertions
require File.expand_path('../../config/environment', __FILE__)
require 'rails/test_help'
...

This will happen because Rails controllers by default handle exceptions and raise the 500 status, making the exceptions invisible to the test suite (which is very helpful if errors are raised in unit tests of a model). Options for disabling this in your test suite, or alternative workarounds, are discussed here.
The key lines of code from that link, which should be added to test/integration/integration_test_helper.rb:
ActionController::Base.class_eval do
def perform_action
perform_action_without_rescue
end
end
Dispatcher.class_eval do
def self.failsafe_response(output, status, exception = nil)
raise exception
end
end
EDIT: I've noticed that that link is quite old now. I'm really familiar with Rack, so whilst the first block looks ok to me, I'm not sure if the second will still be current. You might need to have a look at the relevant current Rails guide if it needs bringing up to date.

Related

Expected Exception but nothing was raised Rails Mailer (the raising-exception method seems not to be called)

I'm having RSpec which expect some exception to be raised. However, when invoking the method, I got error: expected Exception but nothing was raised
my code:
require 'rails_helper'
RSpec.describe ReportMailer, type: :mailer do
let(:csv_file) { "data\n1\n2" }
let(:filename) { 'dummy.csv' }
let(:options) do
{
filename: filename,
to: user.email,
subject: "Transaction Report - #{filename}"
}
end
it 'raise error' do
expect do
ReportMailer.notify(options, nil)
end.to raise_error(SomeExceptions::InvalidFile)
end
end
The problem is that if I just use normal expect call, let's say
expect(described_class.notify(dummy_options, nil)).to eq 1
RSpec show failure/error that I expect before:
Failures:
1) ReportMailer raise error
Failure/Error: raise SomeExceptions::InvalidFile
SomeExceptions::InvalidFile:
The file is invalid
# ./app/mailers/report_mailer.rb:5:in `notify'
# ./spec/mailers/report_mailer_spec.rb:37:in `block (2 levels) in <top (required)>'
My notify method is below:
require 'some_cms_exceptions'
class ReportMailer < ApplicationMailer
def notify(options, csv)
binding.pry
raise SomeExceptions::InvalidFile
validate(options)
attachments[options[:filename]] = { mime_type: 'text/csv', content: csv }
mail(to: options[:to], subject: options[:subject])
end
private
def validate(options)
raise SomeExceptions::InvalidMailOptions unless !options[:to].blank? && !options[:filename].blank?
end
end
I then put binding.pry in notify method and found out that:
if we use expect block, i.e. expect.{...}.to, notify method is not executed.
But if we use normal expect, i.e. expect(...).to ,notify method is executed.
May I know why it behaves like this ? Because other SO questions showed it works by using expect block.
Thank you
At line 5 you are raising SomeExceptions::InvalidFile exception while you are expecting different error in the expectation block
raise_error(SomeExceptions::InvalidMailOptions)
either you replace the exception you are expected or catch all exception using just raise_error without passing any error class(not recommended but for sake of testing).
The answer is as commented by #amit-patel, we need to add deliver_now to really execute mailer RSpec test case.
it 'should send email ' do
expect { ReportMailer.notify(options, nil).deliver_now }.to raise_error(SomeExceptions::InvalidMailOptions)
end

Minitest returns NoMethodError any_instance for JSON:Module

I've been stubbing things fine in Rails5 using mocha and minitest, including custom classes and Net/*. However, I'm now trying to raise JSON::ParserError and I'm getting NoMethodError: undefined method 'any_instance' for JSON:Module.
require 'json'
require 'mocha/mini_test'
JSON.any_instance.stubs(:parse).raises(JSON::ParserError)
I've tried https://github.com/freerange/mocha/issues/218, but didn't work. Not sure what could be so different about JSON that is causing these problems. Also tried using just stub and a do block.
UPDATE:
Here is the code:
begin
parsed_json = JSON.parse(raw_json)
rescue JSON::ParserError
puts 'Exception Caught in Code' # <-- This line prints in the test console.
end
Here is the test code:
test 'should fail because json error' do
JSON.stubs(:parse).raises(JSON::ParserError)
end
This results in an exception being thrown in test should fail because json error, it does not mention the source code. If I add a rescue in the test it will get caught as well:
test 'should fail because json error' do
JSON.stubs(:parse).raises(JSON::ParserError)
rescue JSON::ParserError
puts 'Exception Caught in Test' # <-- This line prints in the test console too.
end

Why is this line raising `cannot load such file` when it should be rescued?

I am attempting to add puffing-billy to a Rails app by following the instructions here.
Running the test suite raises:
in `require': cannot load such file -- capybara/webkit (LoadError)
I am using Poltergeist not Webkit, and so don't need to load this file.
The error originates from the following line in puffing-billy
# /lib/billy/browsers/capybara.rb
DRIVERS = {
poltergeist: 'capybara/poltergeist',
webkit: 'capybara/webkit',
selenium: 'selenium/webdriver'
}
def self.register_drivers
DRIVERS.each do |name, driver|
require driver rescue next # this line should be rescued
send("register_#{name}_driver")
end
end
What could prevent require driver from being rescued and cause this error, and what is a methodical approach to debug this?
In general it's a bad idea to use an inline rescuse in Ruby (having rescue be at the end of the line). Here is a nice article w/ more details: https://www.new-bamboo.co.uk/blog/2015/09/23/dont-inline-rescue-in-ruby/
Inline rescue and a "unfiltered" rescue (without specifying the type of error you want caught) both catch any exceptions that inherit from StandardError LoadError is not a descendant of StandardError so it's is not caught by your inline rescue.
Here is a chart of error hierarchy in Ruby:
http://blog.honeybadger.io/understanding-the-ruby-exception-hierarchy/
So here is your code modified so it will work and also not catch unintentional errors:
DRIVERS.each do |name, driver|
begin
require driver
send("register_#{name}_driver")
rescue LoadError
# This skips to the next driver
# It would be nice to add logging here to notify that the driver was skipped
end
end

Why is Rails.logger nil in test environment?

UPDATE: The following appears to be a result of the Rails environment not being loaded when rake test runs my unit tests (currently only a single module which lives in /lib). Is it possible to include the environment from the command line, instead of changing the rake task itself? Is there a better way to go about this (short of using dependency injection in order to introduce Rails.logger, which is what I'll probably end up doing)?
I'm seeing a NoMethodError in my test environment because of the following call to Rails.logger.error:
# /lib/square.rb
module Square
def self.get_total_sales_for_location(location_id, timeout: 5)
begin
url = "https://connect.squareup.com/v2/locations/#{location_id}/transactions"
headers = {Authorization: "Bearer #{ENV['SQUARE_PERSONAL_ACCESS_TOKEN']}"}
response = RestClient::Request.execute headers: headers,
method: :get,
timeout: timeout,
url: url
if response.code == 200
JSON.parse response.body
else
fail
end
rescue => error
Rails.logger.error "Square::get_total_sales_for_location - #{error.message}"
raise
end
end
end
This all works fine from the console, however running my tests results in: NoMethodError: undefined method 'error' for nil:NilClass
My temporary workaround is to use :try to prevent the exception, but that's very kludgy.
You can do rake environment test. But I would guess it would be nicer to just make the test task dependent on the environment with task test: :environment do

Ruby on Rails Error Handling, Catching Error and Message

I'm trying to figure out the best way to catch a specific error thrown AND the error's message in Ruby on Rails. My use case is that I encounter a timeout error every now and then which is thrown with a general error and I want to treat the timeout error differently than other errors within the same general error. I'm not sure what other type of errors could be thrown in the general error but I assume more of them exist. I have some sample code below of how I'm currently handling it, but I was thinking there might be a better way which I haven't found yet?
tries = 0
begin
tries += 1
<code>
rescue Foo::Bar => e
case e.to_s
when 'More specific timeout error message'
retry unless tries >= 5
else
# Let me see other error messages
log.info("Error: #{e.to_s}")
end
end
You can use multi rescue, to handle different errors.
begin
# DO SOMETHING
rescue Net::Timeout => e # change it to the error your want to catch, check the log.
# handle it
rescue SyntaxError => e # just an example
# handle it
rescue => e # any error that not catch by above rescue go here.
# handle it
end
Read more:
http://phrogz.net/programmingruby/tut_exceptions.html
You can try Rollbar, it help report error on production.
Take a look at retriable gem. It seems like a good fit for what you're proposing. Usually you'd rescue from an specific error type, but retriable also gives you the choice to rescue based on the error message.
begin
Retriable.retriable on: { Foo::Bar => /More specific timeout error message/ }, tries: 3 do
# will retry if an error of type Foo::Bar is raised
# and its message matches /More specific timeout error message/
# code here...
end
rescue => e # rescue for everything else
puts e.message # same as e.to_s
end

Resources