RSpec & Custom matcher with multiple arguments - ruby-on-rails

I'm trying to create a custom matcher for my tests in RoR using RSpec.
define :be_accessible do |attributes|
attributes = attributes.is_a?(Array) ? attributes : [attributes]
attributes.each do |attribute|
match do |response|
response.class.accessible_attributes.include?(attribute)
end
description { "#{attribute} should be accessible" }
failure_message_for_should { "#{attribute} should be accessible" }
failure_message_for_should_not { "#{attribute} should not be accessible" }
end
end
I want to be able to write something like the following in my tests:
...
should be_accessible(:name, :surname, :description)
...
but with the matcher defined above, I must pass an array of symbols instead of symbols separated by commas otherwise the test examines only the first symbol.
Any ideas?

I made it work this way :
RSpec::Matchers.define :be_accessible do |*attributes|
match do |response|
description { "#{attributes.inspect} be accessible" }
attributes.each do |attribute|
failure_message_for_should { "#{attribute} should be accessible" }
failure_message_for_should_not { "#{attribute} should not be accessible" }
break false unless response.class.accessible_attributes.include?(attribute)
end
end
end
I inverted the match and the each loop. I think this is the way Rspec expect it to be, as the block given to the match method is the one executed by Rspec abstract matcher (I guess).
By defining the block with |*attributes|, it takes the list of parameters and turn it into an Array.
So calling should be_accessible(:name, :surname, :description) will work.
By the way, if you just want to check for the existence of attributes, a simple
should respond_to(:name, :surname, :description)
works as well. But it does not looks like for mass-assignement aspect.

Related

RSpec "change all" matcher

I've got some job that updates records, and I want something like:
it 'updates each record' do
expect {
described_class.perform_now
}.to(change_all_of{
Record.pluck(:updated_at)
})
end
Only I can't find anything that looks like how to accomplish this, or what I can recognize as the docs for how to write a custom matcher.
The issue is that change, on an array, will return true if any element has changed; I only want to return true if every element has changed.
Can anyone point me either at whatever I missed that would let me do this, OR, whatever docs/info I need to write my own change matcher?
Alright, thanks to this answer on another question, and staring at the actual code, here's what I've got -
module RSpec
module Matchers
def change_all &block
BuiltIn::ChangeAll.new(nil, nil, &block)
end
module BuiltIn
class ChangeAll < Change
def initialize receiver=nil, message=nil, &block
#change_details = ChangeAllDetails.new(receiver, message, &block)
end
def failure_message
"expected all elements to change, but " + (
#change_details.actual_after & #change_details.actual_before
).collect do |unchanged|
"before[#{#change_details.actual_before.find_index(unchanged)}] " \
"after[#{#change_details.actual_after.find_index(unchanged)}] " \
"#{unchanged}"
end.join(",") + " remained the same"
end
end
class ChangeAllDetails < ChangeDetails
attr_accessor :actual_before
def changed?
!(#actual_after & #actual_before).any?
end
end
end
end
end
Suggestions welcome!

How do I 'expect' a chain of methods using Rspec where the first method takes a parameter?

I have a method call in a ruby model that looks like the following:
Contentful::PartnerCampaign.find_by(vanityUrl: referral_source).load.first
Within the models spec.rb file, I'm trying to mock that call and get a value by passing in a param. But I'm having trouble figuring out the correct way of calling it.
At the top of my spec.rb file I have:
let(:first_double) {
double("Contentful::Model", fields {:promotion_type => "Promotion 1"})
}
Within the describe block I've tried the following:
expect(Contentful::PartnerCampaign).to receive_message_chain(:find_by, :load, :first).
and_return(first_double)
expect(Contentful::PartnerCampaign).to receive_message_chain(:find_by, :load, :first).with(vanityUrl: 'test_promo_path').
and_return(first_double)
expect(Contentful::PartnerCampaign).to receive_message_chain(:find_by => vanityUrl: 'test_promo_path', :load, :first).
and_return(first_double)
As you can probably guess, none of these are working. Does anyone know the correct way to do this sort of thing? Is it even possible?
Generally speaking, I prefer not to use stub chains, as they are often a sign that you are violating the Law of Demeter. But, if I had to, this is how I would mock that sequence:
let(:vanity_url) { 'https://vanity.url' }
let(:partner_campaigns) { double('partner_campaigns') }
let(:loaded_partner_campaigns) { double('loaded_partner_campaigns') }
let(:partner_campaign) do
double("Contentful::Model", fields {:promotion_type => "Promotion 1"}
end
before do
allow(Contentful::PartnerCampaign)
.to receive(:find_by)
.with(vanity_url: vanity_url)
.and_return(partner_campaigns)
allow(partner_campaigns)
.to receive(:load)
.and_return(loaded_partner_campaigns)
allow(loaded_partner_campaigns)
.to receive(:first)
.and_return(partner_campaign)
end
This is what I would do. Notice that I split the "mocking" part and the "expecting" part, because usually I'll have some other it examples down below (of which then I'll need those it examples to also have the same "mocked" logic), and because I prefer them to have separate concerns: that is anything inside the it example should just normally focus on "expecting", and so any mocks or other logic, I normally put them outside the it.
let(:expected_referral_source) { 'test_promo_path' }
let(:contentful_model_double) { instance_double(Contentful::Model, promotion_type: 'Promotion 1') }
before(:each) do
# mock return values chain
# note that you are not "expecting" anything yet here
# you're just basically saying that: if Contentful::PartnerCampaign.find_by(vanityUrl: expected_referral_source).load.first is called, that it should return contentful_model_double
allow(Contentful::PartnerCampaign).to receive(:find_by).with(vanityUrl: expected_referral_source) do
double.tap do |find_by_returned_object|
allow(find_by_returned_object).to receive(:load) do
double.tap do |load_returned_object|
allow(load_returned_object).to receive(:first).and_return(contentful_model_double)
end
end
end
end
end
it 'calls Contentful::PartnerCampaign.find_by(vanityUrl: referral_source).load.first' do
expect(Contentful::PartnerCampaign).to receive(:find_by).once do |argument|
expect(argument).to eq({ vanityUrl: expected_referral_source})
double.tap do |find_by_returned_object|
expect(find_by_returned_object).to receive(:load).once do
double.tap do |load_returned_object|
expect(load_returned_object).to receive(:first).once
end
end
end
end
end
it 'does something...' do
# ...
end
it 'does some other thing...' do
# ...
end
If you do not know about ruby's tap method, feel free to check this out
I think you need to refactor the chain in two lines like this:
model = double("Contentful::Model", fields: { promotion_type: "Promotion 1" })
campaign = double
allow(Contentful::PartnerCampaign).to receive(:find_by).with(vanityUrl: 'test_promo_path').and_return(campaign)
allow(campaign).to receive_message_chain(:load, :first).and_return(model)
Then you can write your spec that will pass that attribute to find_by and check the chain.

Ruby ArgumentError: unknown keyword, but not sure why

I'm trying to make a simple Ruby on Rails plugin. When the redcarpetable function is called with a hash for render_opts, I get "ArgumentError: unknown keyword: render_opts." The code for the function:
def redcarpetable(*fields, renderer: :default, as: [nil], prefix: "rendered", render_opts: {})
fields.each do |field|
if fields.count > 1
define_method "#{prefix}_#{field}" do
Carpet::Rendering.render(read_attribute(field), renderer_opts: render_opts, rc_renderer: renderer).html_safe
end # End defining the method dynamically.
else
if as[0]
as.each do |method_name|
define_method "#{method_name}" do
Carpet::Rendering.render(read_attribute(field), render_opts: render_opts, rc_renderer: renderer).html_safe
end # End defining the method dynamically.
end
else
define_method "rendered_#{field}" do
Carpet::Rendering.render(read_attribute(field), render_opts: render_opts, rc_renderer: renderer).html_safe
end # End defining the method dynamically.
end
end
end # End the fields loop.
end # End the redcarpet method.
How the function is called:
redcarpetable :name, renderer: :simple_parser, as: [:cool_name, :rendered_name], render_opts: {:generate_toc_data: true}
In order to allow for a hash of render options, what must be done to the function declaration? The full code (not documented well or refactored yet) is here.
You call the Carpet::Rendering like this:
Carpet::Rendering.render(read_attribute(field),
render_opts: render_opts, rc_renderer: renderer
).html_safe
But the option is actually called renderer_opts. Just change it to:
Carpet::Rendering.render(read_attribute(field),
renderer_opts: render_opts, rc_renderer: renderer
).html_safe
You might also want to change it in the methods' signature too.
I think your issue might be caused by putting the *fields splat before the other arguments.
Though I'm not specifically sure what's causing your error, you can get an options hash using the following approach:
def redcarpetable(options={}, *fields)
defaults = {
foo: "bar",
bar: "foo"
}
options= defaults.merge(options)
puts options[:foo] # => "bar"
end
This way you can set defaults and override them when you call the method.
In your method body, you're going to have to reference the variables via the options hash,
i.e. options[:foo] and not just foo.
When you call the method, unless your passing nothing to *fields you're going to
have to include the braces in your options argument.
For example:
redcarpetable({foo: bar}, ["field1" "field2"]
And not:
redcarpetable(foo: bar, ["field1, "field2"]
Also, if you're passing any fields but not passing any options, you'll have to include
empty braces:
redcarpetable({}, ["field1", "field2"])

Is there a way to access method arguments in Ruby?

New to Ruby and ROR and loving it each day, so here is my question since I have not idea how to google it (and I have tried :) )
we have method
def foo(first_name, last_name, age, sex, is_plumber)
# some code
# error happens here
logger.error "Method has failed, here are all method arguments #{SOMETHING}"
end
So what I am looking for way to get all arguments passed to method, without listing each one. Since this is Ruby I assume there is a way :) if it was java I would just list them :)
Output would be:
Method has failed, here are all method arguments {"Mario", "Super", 40, true, true}
In Ruby 1.9.2 and later you can use the parameters method on a method to get the list of parameters for that method. This will return a list of pairs indicating the name of the parameter and whether it is required.
e.g.
If you do
def foo(x, y)
end
then
method(:foo).parameters # => [[:req, :x], [:req, :y]]
You can use the special variable __method__ to get the name of the current method. So within a method the names of its parameters can be obtained via
args = method(__method__).parameters.map { |arg| arg[1].to_s }
You could then display the name and value of each parameter with
logger.error "Method failed with " + args.map { |arg| "#{arg} = #{eval arg}" }.join(', ')
Note: since this answer was originally written, in current versions of Ruby eval can no longer be called with a symbol. To address this, an explicit to_s has been added when building the list of parameter names i.e. parameters.map { |arg| arg[1].to_s }
Since Ruby 2.1 you can use binding.local_variable_get to read value of any local variable, including method parameters (arguments). Thanks to that you can improve the accepted answer to avoid evil eval.
def foo(x, y)
method(__method__).parameters.map do |_, name|
binding.local_variable_get(name)
end
end
foo(1, 2) # => 1, 2
One way to handle this is:
def foo(*args)
first_name, last_name, age, sex, is_plumber = *args
# some code
# error happens here
logger.error "Method has failed, here are all method arguments #{args.inspect}"
end
This is an interesting question. Maybe using local_variables? But there must be a way other than using eval. I'm looking in Kernel doc
class Test
def method(first, last)
local_variables.each do |var|
puts eval var.to_s
end
end
end
Test.new().method("aaa", 1) # outputs "aaa", 1
If you need arguments as a Hash, and you don't want to pollute method's body with tricky extraction of parameters, use this:
def mymethod(firstarg, kw_arg1:, kw_arg2: :default)
args = MethodArguments.(binding) # All arguments are in `args` hash now
...
end
Just add this class to your project:
class MethodArguments
def self.call(ext_binding)
raise ArgumentError, "Binding expected, #{ext_binding.class.name} given" unless ext_binding.is_a?(Binding)
method_name = ext_binding.eval("__method__")
ext_binding.receiver.method(method_name).parameters.map do |_, name|
[name, ext_binding.local_variable_get(name)]
end.to_h
end
end
This may be helpful...
def foo(x, y)
args(binding)
end
def args(callers_binding)
callers_name = caller[0][/`.*'/][1..-2]
parameters = method(callers_name).parameters
parameters.map { |_, arg_name|
callers_binding.local_variable_get(arg_name)
}
end
You can define a constant such as:
ARGS_TO_HASH = "method(__method__).parameters.map { |arg| arg[1].to_s }.map { |arg| { arg.to_sym => eval(arg) } }.reduce Hash.new, :merge"
And use it in your code like:
args = eval(ARGS_TO_HASH)
another_method_that_takes_the_same_arguments(**args)
If the function is inside some class then you can do something like this:
class Car
def drive(speed)
end
end
car = Car.new
method = car.method(:drive)
p method.parameters #=> [[:req, :speed]]
If you would change the method signature, you can do something like this:
def foo(*args)
# some code
# error happens here
logger.error "Method has failed, here are all method arguments #{args}"
end
Or:
def foo(opts={})
# some code
# error happens here
logger.error "Method has failed, here are all method arguments #{opts.values}"
end
In this case, interpolated args or opts.values will be an array, but you can join if on comma. Cheers
It seems like what this question is trying to accomplish could be done with a gem I just released, https://github.com/ericbeland/exception_details. It will list local variables and vlaues (and instance variables) from rescued exceptions. Might be worth a look...
Before I go further, you're passing too many arguments into foo. It looks like all of those arguments are attributes on a Model, correct? You should really be passing the object itself. End of speech.
You could use a "splat" argument. It shoves everything into an array. It would look like:
def foo(*bar)
...
log.error "Error with arguments #{bar.joins(', ')}"
end

Conditional code in the define_method block

I am generating some methods on the fly. The method body varies based on a certain criteria.
I was relying on class_eval to generate conditional code.
%Q{
def #{name}
#{
(name != "password") ? "attributes[:#{name}]" :
"encrypt(attributes[:#{name}])"
}
end
}
Recently I have started using define_method. How do I generate conditional code blocks while using define_method?
Edit 1
Here are the possible approaches that I have considered:
1) Checking the name on during run time:
define_method(name) do
if name == password
decrypt(attributes[name])
else
attributes[name]
end
end
This is not a preferred method as the check is done during run time.
2) Conditionally defining the entire method.
if (name == "password")
define_method(name) do
decrypt(attributes[name])
end
else
define_method(name) do
attributes[name]
end
end
This approach has the disadvantage of having to repeat the code block just change a small part (as my actual method has several lines of code).
I think because of closures you can do something like this:
define_method name do
if name=='password'
decrypt(attributes[name])
else
attributes[name]
end
end
But the issue there is that the if will be evaluated on each call to the method.
If you wanted to avoid that you'd need to pass different blocks to define_method for different behavior. e.g.
if name=='password'
define_method(name) { decrypt(attributes[name]) }
else
define_method(name) { attributes[name] }
end
alternately you could pass a lambda chosen by the if statement.
define_method(name, name=='password' ? lambda { decrypt(attributes[name]) } : lambda { attributes[name] }
One thing to think about, define_method can be slower than using eval.

Resources