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

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

Related

Rspec: differences between {} and () in expect [duplicate]

Just learning rspec syntax and I noticed that this code works:
context "given a bad list of players" do
let(:bad_players) { {} }
it "fails to create given a bad player list" do
expect{ Team.new("Random", bad_players) }.to raise_error
end
end
But this code doesn't:
context "given a bad list of players" do
let(:bad_players) { {} }
it "fails to create given a bad player list" do
expect( Team.new("Random", bad_players) ).to raise_error
end
end
It gives me this error:
Team given a bad list of players fails to create given a bad player list
Failure/Error: expect( Team.new("Random", bad_players) ).to raise_error
Exception:
Exception
# ./lib/team.rb:6:in `initialize'
# ./spec/team_spec.rb:23:in `new'
# ./spec/team_spec.rb:23:in `block (3 levels) in <top (required)>'
My question is:
Why does this happen?
What is the difference between the former and later example exactly in ruby?
I am also looking for rules on when to use one over the other
One more example of the same but inverse results, where this code works:
it "has a list of players" do
expect(Team.new("Random").players).to be_kind_of Array
end
But this code fails
it "has a list of players" do
expect{ Team.new("Random").players }.to be_kind_of Array
end
Error I get in this case is:
Failure/Error: expect{ Team.new("Random").players }.to be_kind_of Array
expected #<Proc:0x007fbbbab29580#/Users/amiterandole/Documents/current/ruby_sandbox/tdd-ruby/spec/team_spec.rb:9> to be a kind of Array
# ./spec/team_spec.rb:9:in `block (2 levels) in <top (required)>'
The class I am testing looks like this:
class Team
attr_reader :name, :players
def initialize(name, players = [])
raise Exception unless players.is_a? Array
#name = name
#players = players
end
end
As has been mentioned:
expect(4).to eq(4)
This is specifically testing the value that you've sent in as the parameter to the method. When you're trying to test for raised errors when you do the same thing:
expect(raise "fail!").to raise_error
Your argument is evaluated immediately and that exception will be thrown and your test will blow up right there.
However, when you use a block (and this is basic ruby), the block contents isn't executed immediately - it's execution is determined by the method you're calling (in this case, the expect method handles when to execute your block):
expect{raise "fail!"}.to raise_error
We can look at an example method that might handle this behavior:
def expect(val=nil)
if block_given?
begin
yield
rescue
puts "Your block raised an error!"
end
else
puts "The value under test is #{val}"
end
end
You can see here that it's the expect method that is manually rescuing your error so that it can test whether or not errors are raised, etc. yield is a ruby method's way of executing whatever block was passed to the method.
In the first case, when you pass a block to expect, the execution of the block doesn't occur until it's time to evaluate the result, at which point the RSpec code can catch any error that are raised and check it against the expectation.
In the second case, the error is raised when the argument to expect is evaluated, so the expect code has no chance to get involved.
As for rules, you pass a block or a Proc if you're trying to test behavior (e.g. raising errors, changing some value). Otherwise, you pass a "conventional" argument, in which case the value of that argument is what is tested.

RSpec: Force error within negated custom matcher

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.

How do I test Rails logging from an Rspec feature spec?

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')

rspec expect redirect_to fail

Sometimes new (very DRY) rspec syntax makes me crazy...
Rspec v 2.14.1
describe "POST create" do
subject { post :create, contractor: valid_params }
context "by user" do
before { sign_in #legal.user }
it "contractor successful created" do
expect { subject }.to redirect_to(contractor_path(assigns(:contractor).id))
I have error & question here:
NoMethodError: # :contractor variable not defined
undefined method `id' for nil:NilClass
It seems that expect take an operator before controller method post executes, because I try to raise this method.
My code:
def create
#contractor = Contractor.restrict!(current_accreditation).new(permitted_params) # TODO move to the IR::Base
if #contractor.save
current_accreditation = #contractor.create_legal!(user: current_user) # TODO legal create
redirect_to(#contractor)
else
render(:new)
end
end
Secondly, why I have an error when try
expect(subject).to ...
Why {} works, but () no? In relish docs this method work great: https://www.relishapp.com/rspec/rspec-rails/docs/matchers/redirect-to-matcher
Kinda unrelated but I've found the following helpful:
Use expect {} when you want to test before/after of whatever's in the block. eg. expect { subject } to change(User, :count) - you want to check the count before, then after, to work out the difference and see if it actually changed.
Use expect () to verify the outcome of a block, eg. that a redirect occurred, or something got assigned, etc.
Your test with the assigns(:contractor) doesn't work because you're using the {} notation - so it's trying to work out the assigns(:contractor).id both before and after evaluating the subject (and of course, before the subject, it doesn't exist).

named route as a parameter for example group in rspec

I am newbie in testing and using RSpec and need some help:
I have shared example group:
shared_examples_for 'request which do something' do |opts = {}|
respond.should redirect_to(opts[:redirect_to])
end
In my spec file:
describe "behavior" do
it_should_behave_like 'request which do something', :redirect_to => root_path
end
Looks great, but I get this error:
Exception encountered: #<NameError: undefined local variable or method `root_path' for #<Class:0x000000069e2838>>
and it points on line with 'it_should_behave_like ... '
I tried to include Rails.application.routes.url_helper in spec_helper, but it doesn't work anyway.
By the way it perfectly works from example like this:
describe "behavior" do
it "should redirect" do
response.should redirect_to(root_path)
end
end
(even without explicit including url_helpers)
Thanks for any help.
You can't use path helpers within example groups, but there is a workaround. See this answer:
Passing a named route to a controller macro in RSpec
With this, you can pass a symbol and use send.
In example groups, write Rails.application.routes.url_helpers.root_path
instead of just root_path. You could create your own helper method to
make repeated calls short.

Resources