How to stub a method on a model copy in rspec? - ruby-on-rails

Say I have the next code:
class Foo < ActiveRecord::Base
def self.bar
all.each(&:bar)
end
def bar
# do something that I want stub in test
end
end
Now I want to create test (Rspec):
foo = Foo.create
expect(foo).to receive(:bar)
Foo.bar
This test does not pass because Foo.bar calls other instance of the same model foo.
I wrote some complex code in such situations before, like:
expect_any_instance_of(Foo).to receive(:bar)
but this is not good, because there are no confidence that foo receives message (there could be several instances). And also expect_any_instance_of is not recommended by Rspec maintainers.
How do you test such code, is any best practice?

If you want fine grained control over each instance, you can do something like this:
foo_1 = Foo.create
expect(foo_1).to receive(:bar).and_return(1)
foo_2 = Foo.create
expect(foo_2).to receive(:bar).and_return(2)
# This makes it so our specific instances of foo_1 and foo_2 are returned.
allow(Foo).to receive(:all).and_return([foo_1, foo_2])
expect(Foo.bar).to eq [1, 2]

In your example:
class Foo < ActiveRecord::Base
def self.bar
all.map(&:bar)
end
def bar
# do something that I want stub in test
end
end
If foo = Foo.new
Note that foo.bar is different from Foo.bar.
The former is calling the instance method def bar ( which you want stubed ), while the later is calling the class method def self.bar.
In your test however,
foo = Foo.create
expect(foo).to receive(:bar)
Foo.bar
You are attempting to stub the instance method def bar ( expect(foo).to receive(:bar) ), while you are calling the class method def self.bar ( Foo.bar )
That is why it seems not to work.

Related

ruby monkey patching on the fly

Is there a way to implement monkey patching while an object is being instantiated?
When I call:
a = Foo.new
Prior to the instance being instantiated, I would like to extend the Foo class based on information which I will read from a data store. As such, each time I call Foo.new, the extension(s) that will be added to that instance of the class would change dynamically.
tl;dr: Adding methods to an instance is possible.
Answer: Adding methods to an instance is not possible. Instances in Ruby don't have methods. But each instance can have a singleton class, where one can add methods, which will then be only available on the single instance that this singleton class is made for.
class Foo
end
foo = Foo.new
def foo.bark
puts "Woof"
end
foo.bark
class << foo
def chew
puts "Crunch"
end
end
foo.chew
foo.define_singleton_method(:mark) do
puts "Widdle"
end
foo.mark
are just some of the ways to define a singleton method for an object.
module Happy
def cheer
puts "Wag"
end
end
foo.extend(Happy)
foo.cheer
This takes another approach, it will insert the module between the singleton class and the real class in the inheritance chain. This way, too, the module is available to the instance, but not on the whole class.
Sure you can!
method_name_only_known_at_runtime = 'hello'
string_only_known_at_runtime = 'Hello World!'
test = Object.new
test.define_singleton_method(method_name_only_known_at_runtime) do
puts(string_only_known_at_runtime)
end
test.hello
#> Hello World!
Prior to the instance being instantiated, I would like to extend
Given a class Foo which does something within its initialize method:
class Foo
attr_accessor :name
def initialize(name)
self.name = name
end
end
And a module FooExtension which wants to alter that behavior:
module FooExtension
def name=(value)
#name = value.reverse.upcase
end
end
You could patch it via prepend:
module FooPatcher
def initialize(*)
extend(FooExtension) if $do_extend # replace with actual logic
super
end
end
Foo.prepend(FooPatcher)
Or you could extend even before calling initialize by providing your own new class method:
class Foo
def self.new(*args)
obj = allocate
obj.extend(FooExtension) if $do_extend # replace with actual logic
obj.send(:initialize, *args)
obj
end
end
Both variants produce the same result:
$do_extend = false
Foo.new('hello')
#=> #<Foo:0x00007ff66582b640 #name="hello">
$do_extend = true
Foo.new('hello')
#=> #<Foo:0x00007ff66582b280 #name="OLLEH">

Dynamically defining a Object#initialize for a Ruby class

In my code base, I have a bunch of objects that all adhere to the same interface, which consists of something like this:
class MyTestClass
def self.perform(foo, bar)
new(foo, bar).perform
end
def initialize(foo, bar)
#foo = foo
#bar = bar
end
def perform
# DO SOMETHING AND CHANGE THE WORLD
end
end
The differentiating factor between the classes is the arity of the self.perform and initialize, plus the body of the #perform class.
So, I'd like to be able to create an ActiveSupport::Concern (or just a regular Module if that would work better) which allowed me to do something like this:
class MyTestClass
inputs :foo, :bar
end
which would then use some meta-programming to define self.perform and initialize of the above methods whose airty would depend on the airty specified by the self.inputs method.
Here is what I have so far:
module Commandable
extend ActiveSupport::Concern
class_methods do
def inputs(*args)
#inputs = args
class_eval %(
class << self
def perform(#{args.join(',')})
new(#{args.join(',')}).perform
end
end
def initialize(#{args.join(',')})
args.each do |arg|
instance_variable_set(##{arg.to_s}) = arg.to_s
end
end
)
#inputs
end
end
end
This seems to get the arity of the methods correct, but I'm having a tough time figuring out how to handle the body of the #initialize methods.
Can anybody help me figure out a way that I can successfully meta-program the body of #initialize so it behaves like the example I provided?
You could use this as body for #initialize:
#{args}.each { |arg| instance_variable_set("#\#{arg}", arg) }
However, I wouldn't string eval it. It usually leads to evil things. That said, here is an implementation which gives an incorrect Foo.method(:perform).arity, but still behaves as you would expect:
module Commandable
def inputs(*arguments)
define_method(:initialize) do |*parameters|
unless arguments.size == parameters.size
raise ArgumentError, "wrong number of arguments (given #{parameters.size}, expected #{arguments.size})"
end
arguments.zip(parameters).each do |argument, parameter|
instance_variable_set("##{argument}", parameter)
end
end
define_singleton_method(:perform) do |*parameters|
unless arguments.size == parameters.size
raise ArgumentError, "wrong number of arguments (given #{parameters.size}, expected #{arguments.size})"
end
new(*parameters).perform
end
end
end
class Foo
extend Commandable
inputs :foo, :bar
def perform
[#foo, #bar]
end
end
Foo.perform 1, 2 # => [1, 2]
You were so close! instance_variable_set takes two arguments, first is the instance variable and second is the value you want to set it to. You also need to get the value of the variable, which you can do using send.
instance_variable_set(##{arg.to_s}, send(arg.to_s))

Calling a method inside model

I am trying to trigger a method from inside the model where it is defined. But I am getting an "undefined method `completed_mission_names'" when I try to start my server. Can anybody help me find what I'm doing wrong ?
class MenteeProfile < ActiveRecord::Base
# Update trackable attributes with succeeded missions
MenteeProfile.completed_mission_names
protected
def last_completed_mission_action
end
def self.completed_mission_names
end
end
Simplified to the max, you are trying to do this:
class A
A.foo
def self.foo
puts 'Calling foo!'
end
end
This does not work because the method foo is not defined when you try to invoke it. You must define it first, then you can call it. Like so:
class B
def self.foo
puts 'Calling foo!'
end
B.foo
end
You could also call just foo instead of B.foo from within the class definition. You can add the protected keyword anywhere you like, it will not have any impact on class methods whatsoever.

Rspec:: allow every instance to receive a message

I want to mock a method for every instance of a class.
if I allow_any_instance_of then it works great if instance_count = 1
However if I have many instances of the same class the second instance isn't caught by the mock.
I'm attempting to get a pile of tokens from different sites. But during testing I don't really need "real" tokens. So I plan to mock get_token to return '1111'.
class Foo
def children
[Bar.new, Bar.new] #....
end
def get_tokens
children.map(&:get_token) || []
end
end
so now how do I not mock out the get_tokens?
How about solution like this:
require "spec_helper"
require "ostruct"
class Bar
def get_token
("a".."f").to_a.shuffle.join # simulating randomness
end
end
class Foo
def children
[Bar.new, Bar.new, Bar.new]
end
def get_tokens
children.map(&:get_token) || []
end
end
RSpec.describe Foo do
before do
allow(Bar).to receive(:new).and_return(OpenStruct.new(get_token: "123"))
end
it "produces proper list of tokens" do
expect(Foo.new.get_tokens).to eq ["123", "123", "123"]
end
end
We're stubbing new method on Bar to return something that quacks with get_token (so it behaves like Bar), and it returns a fixed string. This is something you can relay on.
Hope that helps!

Can you override an aliased method in Ruby?

In Ruby, when a method is aliased, the alias points to the body of the original method. So even if you redefine the original method, the alias will continue to use the original definition.
class Foo
def bar
"bar"
end
alias :saloon :bar
end
class Foo
def bar
"BAR"
end
end
puts Foo.new.saloon
will return 'bar' and not 'BAR'. Is there any way to get saloon to use the new definition of bar?
EDIT: I should have been more clear. The example was just an illustration of the issue - it's not the actual problem I need to solve. The issue is more complex when you have chained aliases, for example, in rails' core. E.g. perform_action is aliased by benchmarking module, and then also by flash module. So now a call to perform_action is actually calling perform_action_with_flash which does it's thing, then effectively calls perform_action_with_benchmarking which then calls the original perform_action. If I want to override perform_action_with_benchmarking (even though I agree it's a bad idea - please let's not get into a discussion of that as it's besides the point), I can't because it has been aliased, and as far as I can tell the alias is pointing to what is essentially a copy of the original perform_action_with_benchmarking, so even if I redefine it, there's no effect.
Just re-establish the alias:
class Foo
def bar
"bar"
end
alias :saloon :bar
end
class Foo
def bar
"BAR"
end
alias :saloon :bar
end
puts Foo.new.saloon # => "BAR"
class Foo
def bar
"bar"
end
def saloon
bar
end
end
This is not an alias at all, but it works as you want.
Yes and no. Either coreyward or Sony Santos's solutions work fine. What you need to know is why your coded didn't work the way you though.
alias makes a new name for the function as is appears when the method is invoked. This is not a pointer, but a new way of referring to something. It allows us to do something like this:
class Foo
def bar
"bar"
end
alias :speakeasy :bar
end
class Foo
def bar(secret_code = false)
return speakeasy if secret_code == "A friend of Al"
"Closed because of prohibition!"
end
end
puts Foo.new.bar #=> "Closed because of prohibition!"
puts Foo.new.bar "A friend of Al" #=> "bar"
The old bar still exists, it just a little harder to access now.
Here is another answer, but you have to do some additional steps: collect the aliases before overriding, and realias after:
class Class
def get_aliases method_name
original_proc = instance_method method_name
aliases = []
instance_methods.each do |meth|
# if the methods have different names but they're the same, they're aliased
if meth != method_name.to_s && original_proc == instance_method(meth)
aliases << meth
end
end
aliases
end
end
class Foo
def bar
"bar"
end
alias :saloon :bar
end
class Foo
aliases = get_aliases :bar
def bar
"BAR"
end
aliases.each { |a| alias_method a, :bar }
end
puts Foo.new.saloon #=> BAR
BTW, if anyone can strip off one of that steps, may I know it! :)

Resources