Just wondering if/how arguments can be passed in rspec stub chains. To give an example, suppose I have the following action:
def index
#payments = Payment.order(:updated_at).where(:paid => true)
#bad_payments = Payment.order(:some_other_field).where(:paid => false)
end
In my controller spec, I'd like to be able to stub out both methods and return different results. If only the #payments field were in the action I'd use something like
Payment.stub_chain(:order, :where) { return_this }
But of course, that will return the same value for #bad_payments.
So - in short, how do I include the :updated_at and :paid => true as stub conditions?
You can use this:
Payment.stub_chain(:order, :where).with(:updated_at).with(:paid => true) { return_this }
With rspec > 3 use this syntax:
expect(Converter).to receive_message_chain("new.update_value").with('test').with(no_args)
instead of stub_chain.
Read more about message chains in the documenation. And here is the argument matchers documentation.
You can use nested stub block. The block can accept arguments, and the return value is used as function return value.
I use tap because stub does not returns the callee. The mock created by double is returned as the result of method order, which where method is stub again.
Payment.stub(:order) { |order|
double('ordered_payments').tap { |proxy|
proxy.stub(:where) { |where|
[order, where]
}
}
}
Payment.order(:updated_at).where(:paid => true)
# => returns [:updated_at, {:paid => true}]
Related
class ExternalObject
attr_accessor :external_object_attribute
def update_external_attribute(options = {})
self.external_object_attribute = [1,nil].sample
end
end
class A
attr_reader :my_attr, :external_obj
def initialize(external_obj)
#external_obj = external_obj
end
def main_method(options = {})
case options[:key]
when :my_key
self.my_private_method(:my_key) do
external_obj.update_external_attribute(reevaluate: true)
end
else
nil
end
end
private
def my_private_method(key)
old_value = key
external_object.external_object_attribute = nil
yield
external_object.external_object_attribute = old_value if external_object.external_object_attribute.nil?
end
end
I want to test following for main_method when options[:key] == :my_key:
my_private_method is called once with argument :my_key and it has a block {external_obj.update_external_attribute(reevaluate: true) } , which calls update_external_attribute on external_obj with argument reevaluate: true once.
I'm able to test my_private_method call with :my_key argument once.
expect(subject).to receive(:my_private_method).with(:my_key).once
But how do I test the remaining part of the expectation?
Thank you
It could be easier to answer your question if you post your test as well.
The setup, the execution and asseriotns/expectations.
You can find a short answer in this older question.
You can find useful to read about yield matchers.
I would suggest to mock the ExternalObject if you already haven't. But I can't tell unless you post your actual test code.
I'm going to answer your question. But, then I'm going to explain why you should not do it that way, and show you a better way.
In your test setup, you need to allow the double to yield so that the code will fall through to your block.
RSpec.describe A do
subject(:a) { described_class.new(external_obj) }
let(:external_obj) { instance_double(ExternalObject) }
describe '#main_method' do
subject(:main_method) { a.main_method(options) }
let(:options) { { key: :my_key } }
before do
allow(a).to receive(:my_private_method).and_yield
allow(external_obj).to receive(:update_external_attribute)
main_method
end
it 'does something useful' do
expect(a)
.to have_received(:my_private_method)
.with(:my_key)
.once
expect(external_obj)
.to have_received(:update_external_attribute)
.with(reevaluate: true)
.once
end
end
end
That works. The test passes. RSpec is a powerful tool. And, it will let you get away with that. But, that doesn't mean you should. Testing a private method is ALWAYS a bad idea.
Tests should only test the public interface of a class. Otherwise, you'll lock yourself into the current implementation causing the test to fail when you refactor the internal workings of the class - even if you have not changed the externally visible behavior of the object.
Here's a better approach:
RSpec.describe A do
subject(:a) { described_class.new(external_obj) }
let(:external_obj) { instance_double(ExternalObject) }
describe '#main_method' do
subject(:main_method) { a.main_method(options) }
let(:options) { { key: :my_key } }
before do
allow(external_obj).to receive(:update_external_attribute)
allow(external_obj).to receive(:external_object_attribute=)
allow(external_obj).to receive(:external_object_attribute)
main_method
end
it 'updates external attribute' do
expect(external_obj)
.to have_received(:update_external_attribute)
.with(reevaluate: true)
.once
end
end
end
Note that the expectation about the private method is gone. Now, the test is only relying on the public interface of class A and class ExternalObject.
Hope that helps.
I am trying to build a generic method using meta programming where it uses manipulating methods from array and send to the object using splat, following is the working snippet:
ALLOWED_DATA_TYPES = {
'Integer' => [['to_i']],
'Csv' => [['split', ',']]
}
ALLOWED_DATA_TYPES.each do |type, methods|
define_method("#{type.downcase}_ified_value") do
manipulated_value = value
methods.each { |method| manipulated_value = manipulated_value.public_send(*method) }
return manipulated_value
end
end
It was working great so far, until we decided to add another datatype and it needs to call method on array, for e.g.
"1,2,3".split(',').map(&:to_f)
Now I am stuck, because it's a block. Technically, following code is working alright:
"1,2,3".public_send('split', ',').public_send(:map, &:to_f)
# => [1.0, 2.0, 3.0]
But adding that block to array is throwing error
[['split', ','], ['map', &:to_f]]
# => SyntaxError: syntax error, unexpected &, expecting ']'
I know I can create a proc and call it with amp & but I hope u get it that it is loosing consistency, I need something that will just work with splat operator as defined using #define_method
I am out of ideas now, please help.
You're out of luck, & is not an operator - it is a special syntax that is only allowed in function parameters (both definition and invocation). One way you could do it is to reserve the last element of the array for the block; but then you always have to use it (even if it is just nil).
methods.each { |*method, proc| manipulated_value = manipulated_value.public_send(*method, &proc) }
This should work with [['split', ',', nil], ['map', :to_f]].
Off-topic, but note that these three lines can be more succintly rewritten using inject:
manipulated_value = value
methods.each { |*method, proc| manipulated_value = manipulated_value.public_send(*method, &proc) }
return manipulated_value
becomes
methods.inject(value) { |manipulated_value, (*method, proc)| manipulated_value.public_send(*method, &proc) }
I have some variables that I get from the Get/Post requests =>
params[:status], params[:value] #:value has the value of the :status
and I would love to use the update method of the active record to update the record (validation needs to be run). How can I then create a dynamic query like
#user.update(status: value)
I have tried this.
attribute = { params[:status] => params[:value] }
user.update(attribute)
but this does not call any validation. Validation is IMPORTANT for me here.
Update will call validation, but you need to change how you're building the attribute, as params[:status]: is not a valid construct. Use....
attribute = { params[:status] => params[:value] }
You can use send method to call a method (attribute) dynamically on your object
http://ruby-doc.org/core-2.3.2/Object.html#method-i-send
status = params[:status]
user.send("#{status}=", params[:value])
user.save
There are 2 ways of doing this
attribute = { status: value }
user.update(attribute) // this should run validation for sure.
user.assign_attributes(attribute)
user.save(validate: true)
If more than one field and value separate with a comma
attributes = {"firstname"=>"John", "surname"=>"Stack"}
In your case
attributes = {params[:status] => params[:value], params[:name] => params[:value]}
Or convert to string if need be
attributes = {params[:status].to_s => params[:value].to_s, params[:status].to_s => params[:value].to_s} //default is string
Is it possible to write a scope with optional arguments so that i can call the scope with and without arguments?
Something like:
scope :with_optional_args, lambda { |arg|
where("table.name = ?", arg)
}
Model.with_optional_args('foo')
Model.with_optional_args
I can check in the lambda block if an arg is given (like described by Unixmonkey) but on calling the scope without an argument i got an ArgumentError: wrong number of arguments (0 for 1)
Ruby 1.9 extended blocks to have the same features as methods do (default values are among them):
scope :cheap, lambda{|max_price=20.0| where("price < ?", max_price)}
Call:
Model.cheap
Model.cheap(15)
Yes. Just use a * like you would in a method.
scope :print_args, lambda {|*args|
puts args
}
I used scope :name, ->(arg1, arg2 = value) { ... } a few weeks ago, it worked well, if my memory's correct. To use with ruby 1.9+
You can conditionally modify your scope based on a given argument.
scope :random, ->(num = nil){ num ? order('RANDOM()').limit(num) : order('RANDOM()') }
Usage:
Advertisement.random # => returns all records randomized
Advertisement.random(1) # => returns 1 random record
Or, you can provide a default value.
scope :random, ->(num = 1000){ order('RANDOM()').limit(num) }
Usage:
Product.random # => returns 1,000 random products
Product.random(5) # => returns 5 random products
NOTE: The syntax shown for RANDOM() is specific to Postgres. The syntax shown is Rails 4.
Just wanted to let you know that according to the guide, the recommended way for passing arguments to scopes is to use a class method, like this:
class Post < ActiveRecord::Base
def self.1_week_before(time)
where("created_at < ?", time)
end
end
This can give a cleaner approach.
Certainly.
scope :with_optional_args, Proc.new { |arg|
if arg.present?
where("table.name = ?", arg)
end
}
Use the *
scope :with_optional_args, -> { |*arg| where("table.name = ?", arg) }
You can use Object#then (or Object#yield_self, they are synonyms) for this. For instance:
scope :cancelled, -> (cancelled_at_range = nil) { joins(:subscriptions).merge(Subscription.cancelled).then {|relation| cancelled_at_range.present? ? relation.where(subscriptions: { ends_at: cancelled_at_range }) : relation } }
I am using Ruby on Rails 3.0.9, RSpec-rails 2 and FactoryGirl. I am trying to check the kind_of?, to count, ... items present in an array using\inside the RSpec its feature. That is, I would like to make something like the following:
subject do
Factory(
:user,
:articles => 3.times.inject([]) { |articles,i|
articles << Factory(
:article,
:user_id => Factory(:user)
)
}
end
# Note the (wrong!) usage of 'first', 'count' methods
its(:articles) { first.should be_kind_of(Article) }
its(:articles) { count.should == 10 }
But, since I get errors, it seems not possible to make that in the (wrong!) way I made it in the above code. Is there a way to do that (to check the kind_of?, count, ...)?
http://rdoc.info/github/rspec/rspec-core/master/RSpec/Core/Subject/ClassMethods:its:
describe Person do
subject do
Person.new.tap do |person|
person.phone_numbers << "555-1212"
end
end
its("phone_numbers.first") { should eq("555-1212") }
end
so in your case:
its('articles.first') { should be_kind_of(Article) }
its('articles.count') { should == 10 }