RSpec: Force error within negated custom matcher - ruby-on-rails

I have a custom matcher, that has an expect within:
RSpec::Matchers.define :have_access_to do |action|
match do |user|
allow(controller).to receive(:authorize!)
# perform GET request...
expect(controller).to have_received(:authorize!)
response.code == "200"
end
end
This works, as long as I call it this way
it { expect(shop_manager).to have_access_to(:index) }
But when I try to use the negated form not_to, and the expect within the customer matcher fails, the test passes:
it { expect(shop_manager).not_to have_access_to(:index) }
I understand RSpec logic here: When you want the test to fail with not_to and it fails, everything is fine.
But this expect serves as a side condition: I want to check if the whole request has passed the authorize! step.
I know that a matcher should only test one thing, but I use it a lot and it would lead to a lot of code duplication when I add the have_received(:authorize!) check to every single test.
Is there a way to force RSpec to fail, no matter if the call is negated or not?

You could do
RSpec::Matchers.define :have_access_to do |action|
match do |user|
allow(controller).to receive(:authorize!)
# perform GET request...
fail "failed to receive authorize" unless controller.received_message?(:authorize!)
response.code == "200"
end
end
Using old rspec shoulda syntax. However, I get a deprecation warning for this
Using received_message? from rspec-mocks' old :should syntax without explicitly enabling the syntax is deprecated.

Related

How to use double to test exception handling in unit testing in ruby on rails?

I want to test my send method logic in my Client class. I have this so far in my client_spec file. How can i raise and error and test that rescue is called and the error is logged in my spec file.
I am new to rspec but I believe i can use a test double for logging instead of calling actual logger.
client_spec.rb
describe Client do
describe '#send' do
let (:subject) {Client.new}
it 'raises and logs the exception'
//how to test raising and logging of the error
end
end
end
client.rb
class Client
include HTTParty
base_uri "https://www.example.com"
format :json
def send
begin
response = HTTParty.get(url)
if response.successful?
response
else
raise 'invalid response'
end
rescue HTTParty::Error => e
logger.warn(e.message)
end
end
end
yes, you can stub both HTTParty and logger, something like
let(:logger) { double('Logger') }
let(:error) { HTTParty::Error.new('foo') }
before do
allow(Logger).to receive(:new) { logger }
end
(Not sure what kind of logger are you using)
and then you can tell HTTParty to raise the kind of error you're expecting, like:
context 'when there is a HTTParty error' do
before do
allow(HTTParty).to receive(:get).and_raise(error)
end
it 'logs the error' do
expect(logger).to receive(:warn).with('foo')
subject.send
end
end
Ok, so, the expect before the send is a way to test if the tested method will "trigger" or perform additional operations, it's a way to say "when I execute foo, I expect that "this" happened". There is a way to declare way around, like:
subject.send
expect(logger).to have_received(:warn).with('foo')
But I think this way is newer than the one I proposed, I just got really used to use the proposed one.
About mocks and stubs, yeah, you don't test them because those objects are out of the scope of the class you're testing on. So, Logger should have its own set of tests, the same for HTTParty, so, you "simulate" their behaviors in order to test your class, that way you'll remove the dependency between the test and other libraries (or classes).
When your unit test is done, then you can move to an integration test, and test (sorry for the redundancy) that the whole stack (or endpoint or controller or "flow") is doing what you're expecting to do.

Ruby o Rails Rspec - How to validate arguments when creating custom matchers

I would like to create a validation like the matcher have_http_status to the content_type.
I created the following matcher:
require 'rspec/expectations'
RSpec::Matchers.define :have_content_type do |expected|
match do |actual|
actual.content_type == expected
end
description do
"respond with content_type #{expected}"
end
end
However, I would like to check if the actual value is a response object. If not, I would like to give a message like the have_http_status, which would be:
Failure/Error: it { expect(legal_person).to have_http_status(200) }
expected a response object, but an instance of LegalPerson was received
# ./spec/controllers/legal_people_controller_spec.rb:105:in `block (5 levels) in <top (required)>'
So when I pass an object different from a request-response object, I would expect an error saying that a response object was expected.
It works without it but would be better if it shows an informative message saying what went wrong exactly.
Thanks in advance for the help.
You're trying to have 2 matchers under 1 name. I believe this is a bad practice, and there should be two matchers - one for content type, another one - for checking request/response type. So your tests should look like:
expect(response).to be_a(ActionDispatch::Response)
expect(response).to have_content_type('application/json')
Also, I don't see any problem of having just content type check. If the object passed does not have .content_type method, the matcher will throw corresponding error: Undefined method 'content_type' and you should be fine figuring out that you've passed wrong object.
But, if you are still sure you need to check two things in one matcher, check this:
RSpec::Matchers.define :have_content_type do |expected|
match do |actual|
request_or_response?(actual) && actual.content_type.to_s == expected
end
description do |actual|
if request_or_response?(actual)
"respond with content_type #{expected}"
else
"a response object, but an instance of #{actual.class} was received"
end
end
private
def request_or_response?(actual)
[Rack::Request, ActionDispatch::Response].any? do |klass|
actual.is_a?(klass)
end
end
end

Test for HTTP status code in some RSpec rails request exampes, but for raised exception in others

In a Rails 4.2.0 application tested with rspec-rails, I provide a JSON web API with a REST-like resource with a mandatory attribute mand_attr.
I'd like to test that this API answers with HTTP code 400 (BAD REQUEST) when that attribute is missing from a POST request. (See second example blow.) My controller tries to cause this HTTP code by throwing an ActionController::ParameterMissing, as illustrated by the first RSpec example below.
In other RSpec examples, I want raised exceptions to be rescued by the examples (if they're expected) or to hit the test runner, so they're displayed to the developer (if the error is unexpected), thus I do not want to remove
# Raise exceptions instead of rendering exception templates.
config.action_dispatch.show_exceptions = false
from config/environments/test.rb.
My plan was to have something like the following in a request spec:
describe 'POST' do
let(:perform_request) { post '/my/api/my_ressource', request_body, request_header }
let(:request_header) { { 'CONTENT_TYPE' => 'application/json' } }
context 'without mandatory attribute' do
let(:request_body) do
{}.to_json
end
it 'raises a ParameterMissing error' do
expect { perform_request }.to raise_error ActionController::ParameterMissing,
'param is missing or the value is empty: mand_attr'
end
context 'in production' do
###############################################################
# How do I make this work without breaking the example above? #
###############################################################
it 'reports BAD REQUEST (HTTP status 400)' do
perform_request
expect(response).to be_a_bad_request
# Above matcher provided by api-matchers. Expectation equivalent to
# expect(response.status).to eq 400
end
end
end
# Below are the examples for the happy path.
# They're not relevant to this question, but I thought
# I'd let you see them for context and illustration.
context 'with mandatory attribute' do
let(:request_body) do
{ mand_attr: 'something' }.to_json
end
it 'creates a ressource entry' do
expect { perform_request }.to change(MyRessource, :count).by 1
end
it 'reports that a ressource entry was created (HTTP status 201)' do
perform_request
expect(response).to create_resource
# Above matcher provided by api-matchers. Expectation equivalent to
# expect(response.status).to eq 201
end
end
end
I have found two working and one partially working solutions which I'll post as answers. But I'm not particularly happy with any of them, so if you can come up with something better (or just different), I'd like to see your approach! Also, if a request spec is the wrong type of spec to test this, I'd like to know so.
I foresee the question
Why are you testing the Rails framework instead of just your Rails application? The Rails framework has tests of its own!
so let me answer that pre-emptively: I feel I'm not testing the framework itself here, but whether I'm using the framework correctly. My controller doesn't inherit from ActionController::Base but from ActionController::API and I didn't know whether ActionController::API uses ActionDispatch::ExceptionWrapper by default or whether I first would have had to tell my controller to do so somehow.
You'd want to use RSpec filters for that. If you do it this way, the modification to Rails.application.config.action_dispatch.show_exceptions will be local to the example and not interfere with your other tests:
# This configure block can be moved into a spec helper
RSpec.configure do |config|
config.before(:example, exceptions: :catch) do
allow(Rails.application.config.action_dispatch).to receive(:show_exceptions) { true }
end
end
RSpec.describe 'POST' do
let(:perform_request) { post '/my/api/my_ressource', request_body }
context 'without mandatory attribute' do
let(:request_body) do
{}.to_json
end
it 'raises a ParameterMissing error' do
expect { perform_request }.to raise_error ActionController::ParameterMissing
end
context 'in production', exceptions: :catch do
it 'reports BAD REQUEST (HTTP status 400)' do
perform_request
expect(response).to be_a_bad_request
end
end
end
end
The exceptions: :catch is "arbitrary metadata" in RSpec speak, I chose the naming here for readability.
Returning nil from a partially mocked application config with
context 'in production' do
before do
allow(Rails.application.config.action_dispatch).to receive(:show_exceptions)
end
it 'reports BAD REQUEST (HTTP status 400)' do
perform_request
expect(response).to be_a_bad_request
end
end
or more explicitly with
context 'in production' do
before do
allow(Rails.application.config.action_dispatch).to receive(:show_exceptions).and_return nil
end
it 'reports BAD REQUEST (HTTP status 400)' do
perform_request
expect(response).to be_a_bad_request
end
end
would work if that was the only example being run. But if it was, we could just as well drop the setting from config/environments/test.rb, so this is a bit moot. When there are several examples, this will not work, as Rails.application.env_config(), which queries this setting, caches its result.
Mocking Rails.application.env_config() to return a modified result
context 'in production' do
before do
# We don't really want to test in a production environment,
# just in a slightly deviating test environment,
# so use the current test environment as a starting point ...
pseudo_production_config = Rails.application.env_config.clone
# ... and just remove the one test-specific setting we don't want here:
pseudo_production_config.delete 'action_dispatch.show_exceptions'
# Then let `Rails.application.env_config()` return that modified Hash
# for subsequent calls within this RSpec context.
allow(Rails.application).to receive(:env_config).
and_return pseudo_production_config
end
it 'reports BAD REQUEST (HTTP status 400)' do
perform_request
expect(response).to be_a_bad_request
end
end
will do the trick. Note that we clone the result from env_config(), lest we modify the original Hash which would affect all examples.
context 'in production' do
around do |example|
# Run examples without the setting:
show_exceptions = Rails.application.env_config.delete 'action_dispatch.show_exceptions'
example.run
# Restore the setting:
Rails.application.env_config['action_dispatch.show_exceptions'] = show_exceptions
end
it 'reports BAD REQUEST (HTTP status 400)' do
perform_request
expect(response).to be_a_bad_request
end
end
will do the trick, but feels kinda dirty. It works because Rails.application.env_config() gives access to the underlying Hash it uses for caching its result, so we can directly modify that.
In my opinion the exception test does not belong in a request spec; request specs are generally to test your api from the client's perspective to make sure your whole application stack is working as expected. They are also similar in nature to feature tests when testing a user interface. So because your clients won't be seeing this exception, it probably does not belong there.
I can also sympathize with your concern about using the framework correctly and wanting to make sure of that, but it does seem like you are getting too involved with the inner workings of the framework.
What I would do is first figure out whether I am using the feature in the framework correctly, (this can be done with a TDD approach or as a spike); once I understand how to accomplish what I want to accomplish, I'd write a request spec where I take the role of a client, and not worry about the framework details; just test the output given specific inputs.
I'd be interested to see the code that you have written in your controller because this can also be used to determine the testing strategy. If you wrote the code that raises the exception then that might justify a test for it, but ideally this would be a unit test for the controller. Which would be a controller test in an rspec-rails environment.

Rspec stubbing return values from a given block

I am trying to get to grips with stubbing in Rspec. I would like to understand how to stub returns values from an array.
Here is what I am attempting to stub at the moment,
if client.jobs.any?
client.jobs.map do |job|
if job.job_locations.any?
job.job_locations.map do |jl|
if jl.location_id == self.location_id
errors.add(:location_id, "This location is in use with another of the client's jobs")
return false
end
end
end
end
end
I can stub the first line (that there the client has jobs) but I am not sure how to stub the return values of the array so that they run in the spec tests.
here is the relevant snippet
context "location_id matches job_location location_id" do
before do
allow(client_location).to receive(:client).and_return(client)
allow(client).to receive(:jobs).and_return(some_jobs)
allow(some_jobs).to receive(:map).and_return([job])
#eventually I want to get to this
allow_any_instance_of(JobLocation).to receive(:location_id).and_return(1)
allow_any_instance_of(ClientLocation).to receive(:location_id).and_return(1)
end
it "returns false" do
expect(#instance.destroy).to be_falsey
end
end
I should add the 'some_jobs' variables in the spec test are factory girl generated instances. Some jobs would equal [some_jobs].
If you define some_jobs to be [job] (e.g. via RSpec's let mechanism), then you don't need to redefine map and your production code will continue to execute. By stubbing out some_jobs.map, you basically bypassed the rest of your production code.
As an aside, the definitions of client_location, client, some_jobs and job are all highly relevant to the code snippet you shared and should be included.

Clean Rspec matcher for change(Model, :count).by(1)

I'm working hard trying to keep my spec files as clean as possible. Using 'shoulda' gem and writing customized matchers that follow the same pattern.
My question is about creating a custom matcher that would wrap expect{ post :create ... }.to change(Model, :count).by(1) and could be used in the same example groups with other 'shoulda' matchers. Details bellow:
Custom matcher (simplified)
RSpec::Matchers.define :create_a_new do |model|
match do |dummy|
::RSpec::Expectations::ExpectationTarget.new(subject).to change(model, :count).by(1)
end
end
Working example
describe 'POST create:' do
describe '(valid params)' do
subject { -> { post :create, model: agency_attributes } }
it { should create_a_new(Agency) }
end
end
This work OK as long as I use a subject lambda and my matcher is the only one in the example group.
Failing examples
Failing example 1
Adding more examples in the same group makes the other matcher fail because subject is now a lambda instead of an instance of the Controller.
describe 'POST create:' do
describe '(valid params)' do
subject { -> { post :create, model: agency_attributes } }
it { should create_a_new(Agency) }
it { should redirect_to(Agency.last) }
end
end
Failing example 2
The 'shoulda' matcher expect me to define a before block, but this become incompatible with my custom matcher
describe 'POST create:' do
describe '(valid params)' do
before { post :create, agency: agency_attributes }
it { should create_a_new(Agency) }
it { should redirect_to(Agency.last) }
end
end
Expected result
I am looking for a way to write my custom matcher that would fit in the same example group as other matchers, meaning my custom matcher should use the before block to execute the controller action, the "failing example #2" above is the way I would like to write my specs. Is it possible?
Thanks for reading
I do not think there is a way you can get your failing examples passing.
This is because change really needs a lambda, since it needs to perform your count twice (once before, and once after calling it). That's the reason I tend not to use it (or use it in context isolation).
What I usually do, instead of using the count matcher, is checking three things:
The record is persisted. If I assign the model to #model, then I use expect(assigns(:model)).to be_persisted
The record is an instance of the expected model (though might not seem useful, it is
quite descriptive when using an STI). expect(assigns(:model)).to be_a(Model).
Check the last record in DB is the same as the one I just create `expect(assigns(:model)).to eq(Model.last)``
And that's the way I usually test the change matcher without using it. Of course, you can now create your own matcher
RSpec::Matchers.define :create_a_new do |model|
match do |actual|
actual.persisted? &&
actual.instance_of?(Participant) &&
(Participant.last == actual)
end
end

Resources