I have a Rails controller which does a health check of the database, like this:
def health_check
begin
status = ActiveRecord::Base.connected? ? 'UP' : 'DOWN'
rescue
status = 'DOWN'
end
render text: status
end
I'm trying to create a RSpec controller spec for this, and the specs for positive and negative responses work, but when I try to test the rescue block, RSpec seems to be ignoring it:
RSpec.describe(HealthCheckController) do
context 'When the check raises an exception' do
before :each do
allow(ActiveRecord::Base).to receive(:connected?).and_raise(OCIException) # Using Oracle
end
it 'should render text DOWN' do
# First attempt
get :health_check
expect(response.body).to eq 'DOWN'
# Second attempt
expect { get :health_check }.to raise_error
expect(response.body).to eq 'DOWN'
end
end
end
I tried the spec with both of the code inside the it block above (separatedly).
For the first, RSpec failed with this:
Failure/Error: get :health_check
OCIException:
OCIException
For the second, it also failed, with this more "familiar" message instead:
Failure/Error: expect(response.body).to eq 'DOWN'
expected: "DOWN"
got: ""
(compared using ==)
I also checked the HTTP code being returned by response, and it's 200, so the response itself is fine, no 500 error.
It's as if RSpec is simply bypassing the rescue block and not running it. What may be causing this? I'm not using the bypass_rescue RSpec method anywhere, this is also a new project.
Using:
Rails 4.2.6
Rake 10.5.0
RSpec-core 3.3.2
RSpec-rails 3.3.3
Actually, the problem has nothing to do with the rescue block in your controller. Rather, it is caused by the fact that you have overriden ActiveRecord::Base#connected? by stubbing it in your before block.
Calling render in the controller initiates a connection to the database. Somewhere in the process ActiveRecord::Base#connected? gets called (twice, actually), but instead of returning what it is supposed to return, it raises an exception that you have defined in your setup.
In your first example, the exception is raised before your expectation, thus the explicit exception name in the failure message.
In your second example, the exception is suppressed by your raise_error expectation, so RSpec is able to proceed to the next one. Where it fails because the render call in the controller never runs (due to the error), and as a result, the response body never gets a chance to be populated.
As a confirmation that ActiveRecord::Base#connected? does get called when you call render, try running the following spec. You will see that it's green, meaning that the said method has been called twice in the process. You can also try replacing render with head :ok and will see that in this case ActiveRecord::Base#connected? gets called only once.
RSpec.describe ApplicationController do
controller do
def index
render json: ''
end
end
specify 'test' do
expect(ActiveRecord::Base).to receive(:connected?).twice
get :index
end
end
Related
I got a method to update the person by id:
def update_person(id)
handle_exceptions do
person = Person.find(id)
#...other
end
end
When this id doesn't exist, the handle_exception should be called. But how could I test it? The test I wrote is:
context 'not found the proposals' do
subject {controller.send(:update_person, 3)}
before do
allow(Person).to receive(:find).and_raise(ActiveRecord::RecordNotFound)
allow(subject).to receive(:handle_exceptions)
end
it 'calls handle_exceptions' do
expect(subject).to have_received(:handle_exceptions)
end
end
But it not works, I got a failure said:
Failure/Error: expect(subject).to have_received(:handle_exceptions)
({:message=>"Not Found", :status=>:not_found}).handle_exceptions(*(any args))
expected: 1 time with any arguments
received: 0 times with any arguments
The handle_exceptions method is
def handle_exceptions
yield
rescue ActiveRecord::RecordNotFound => e
flash[:warning] = 'no record found'
Rails.logger.error message: e.message, exception: e
#error_data = { message: 'no record found', status: :not_found }
end
The problem is that you are calling the method under test in the subject block.
subject {controller.send(:update_person, 3)}
This is actually called before the example runs and before the before block.
context 'not found the proposals' do
before do
allow(subject).to receive(:handle_exceptions)
end
it 'calls handle_exceptions' do
controller.send(:update_person, "NOT A VALID ID")
expect(subject).to have_received(:handle_exceptions)
end
end
But as far as tests go this one is not good. You're testing the implementation of update_person and not the actual behavior. And you're calling the method with update_person.send(:update_person, 3) presumably to test a private method.
You should instead test that your controller returns a 404 response code when try to update with an invalid id. Also why you insist on stubbing Person.find is a mystery since you can trigger the exception by just passing an invalid id. Only stub when you actually have to.
After couple days working, I realized the reason I'm confused about it is I didn't figure out about 'who called this function', and I think it's the most important thing to know before test it. For the method like this:
class User::Controller
def methodA
methodB
end
def methodB
// ...
end
The mistake that I made is I thought the methodB is called by methods, but it's not. It's called by the controller, and that's the reason that I can't make the test works. There's so many things need to learn, and I hope there's one day that I won't have a mistake like this and be able to help others.
I need to solve a test of my app. Coverage is complaining about a line of code that evaluates the connection to MongoDB (rescue Mongo::Error::NoServerAvailable => _e) and renders the error.
What do you think I should use to test this:
def index
render json: Complex.all.to_json
rescue Mongo::Error::NoServerAvailable => _e
render json: { error_description: 'no database available' }, status: 503
end
I am trying to test with something like:
it 'should return an exception' do
get :index
expect(response).to raise_exception
end
I found that I should use
.and_raise(IOError)
But I am not sure where to use it to make it fall on the line.
Actually I can make it fall on the exception if I stop Mongo, but that's not the idea.
Thanks for your time.
To reach the line of code that handles the exception, stub Complex.all.to_json to raise the exception. Since Complex.all.json is chained it takes a little extra effort to stub it. Also, since the exception is handled, you can't test that it's raised; instead, test the result of handling it.
it 'should handle the exception' do
all = double
allow(all).to receive(:to_json).and_raise Mongo::Error::NoServerAvailable
allow(Complex).to receive(:all).and_return all
get :index
expect(response.status).to eq(503)
expect(response.body).to include('no database available')
# you could test the JSON more thoroughly, but you get the idea
end
You could use receive_message_chain to stub Complex.all.to_json with less code. I used the long version since it's easier to understand what's going on.
This question tells me how to test logger statements from RSpec model and controller specs. Unfortunately, it doesn't seem to work from a feature spec. With this code:
# controller.rb
def action
logger.info 'foobar'
end
# spec.rb
scenario 'logging should work' do
expect(Rails.logger).to receive(:info).with('foobar')
visit action_path
end
I get the error:
Failure/Error: visit action_path
#<ActiveSupport::Logger:0x007ff45b6e5ad0> received :info with unexpected arguments
expected: ("foobar")
got: (no args)
The test.log file does not contain foobar, so it seems the test is failing immediately, before the controller action has a chance to complete.
Is there some way to use this expect(Rails.logger) syntax in a feature spec?
The Rails.logger.info method can take a string or a block. If you're invoking the block form then it will give this "got: (no args)" output.
For example
logger.info 'foobar'
...all on one line will call .info with a string, but if you do
logger.info
"foobar foobar longer message so I'll put it on its own line"
split across two lines without brackets, then you're actually passing a block. Add some brackets...
logger.info(
"foobar foobar longer message so I'll put it on its own line"
)
...and you're back to a string.
He says knowingly after bashing his head on this problem for a few hours :-)
Before realising that, I started figuring out how to mock the Rails.logger class. That might be a useful approach for you or others. Maybe you're calling with a block for some other reason (something to do with feature vs controller specs?), or maybe you can't change the calling code, in which case... something like this might be a useful starting point:
class LoggerMock < Object
def initialize; end
def info(progname = nil, &block)
mock_info(block.call)
end
end
and
logger_mock = LoggerMock.new
allow(Rails).to receive(:logger).and_return(logger_mock)
expect(logger_mock).to receive(:mock_info).with('foobar')
I want to check some internal behaviour of method #abc which also raises an error.
def abc
String.class
raise StandardError
end
describe '#abc' do
it 'should call String.class' do
String.should_receive(:class)
end
end
String.class - is just an example of any method call of any class which I want to perform inside this method.
But I got an error:
Failure/Error: #abc
StandardError
How I can mute this exception so this spec would pass?
You cannot "mute" the exception; you can only catch it, either explicitly through a rescue clause or implicitly through an expectation. Note that this is different than substituting a test double for a called method. The exception is still getting raised by the code under test.
If you don't care whether an error is raised, then you can use:
abc rescue nil
to invoke the method. (Note: This will implicitly only catch StandardError)
If you want to using the should or expect syntax, you need to place the code which is going to raise the error within a block, as in the following:
expect {abc}.to raise_error(StandardError)
Combining this with the setting of an expectation that String.class be invoked, you get:
describe '#abc' do
it 'should call String.class' do
expect(String).to receive(:class)
expect {abc}.to raise_error(StandardError)
end
end
Say I have a test
describe "import stuff"
it "should import correctly"
imported_count = import_stuff()
assert_equal 3, imported_count
end
end
How do I log the database state e.g. puts ImportedStuff.all.to_json when the test fails?
I know I can specify the error message like this assert_equals 3, imported_count, 'my message'. But I don't want to invoke ImportedStuff.all.to_json when the tests succeed. assert_equals seems to only accept a string for the error message.
Although it doesn't appear to be documented you can pass a Proc as a message. The proc passed will be invoked only on failure.
Example:
def print_db_state
Proc.new { puts DimensionType.all.to_json }
end
describe "import stuff"
it "should import correctly"
imported_count = import_stuff()
assert_equal 3, imported_count, print_db_state
end
end
Passing ImportedStuff.all.to_json will invoke all.to_json every time the assertion is executed even if the test doesn't fail - not good.
Minitest prints passed message only if assertion had failed ( https://github.com/seattlerb/minitest/blob/master/lib/minitest/assertions.rb#L127 ). You can pass "ImportedStuff.all.to_json" and don't afraid that it would be calculated for successful assertion.
You could also pass a parameterless Proc to all minitest assertions as message ( instead of String ), it would be called when test fails and it's result would be printed as message.
You usually don't need to log the database status for a test. Tests help you determine if everything is ok, or if there's something that needs your attention. If the test fails, then you can always re-run it adding a log for the database.
If you want the log to be there, I'd say to add an if:
imported_count = import_stuff()
if imported_count==3
puts ImportedStuff.all.to_json
end
assert_equal 3, imported_count
Another option you have, is to put the logging in an after block.
EDIT
If you want to log your errors if your test fails, you can add:
def teardown
unless passed?
puts ImportedStuff.all.to_json
end
end