Rspec matcher to test a method calls another inside an each block - ruby

I'm trying to test that a ruby method calls another method from inside it, in an each block
Sample classes
class A
def foo
return 'foo'
end
end
class B
def initialize
#array_of_class_a_instances = []
end
def bar
#array_of_class_a_instances.each do |element|
element.foo
end
end
end
I want to write a unit test for the bar method inside the B class to verify that the foo method is being called for the amount of times that #array_of_class_a_instances is long. If we say that the array has 4 elements, I want to test that the foo method is being called 4 times. I have a test that works below, but it uses multiple expect statements. Is there some syntax where it could be written with just one?
describe B do
describe '#bar' do
# ommitted for brevity
small_b = B.new
it 'calls the foo method for each element in the array' do
expect(small_b.array_of_class_a_instances[0]).to receive(:foo).exactly(1).time
expect(small_b.array_of_class_a_instances[1]).to receive(:foo).exactly(1).time
expect(small_b.array_of_class_a_instances[2]).to receive(:foo).exactly(1).time
expect(small_b.array_of_class_a_instances[3]).to receive(:foo).exactly(1).time
small_b.bar
end
end
end

You can use each in your test as well:
small_b.array_of_class_a_instances.each do |instance|
expect(instance).to receive(:foo).exactly(1).time
end
You can also use the all matcher to check all elements of an array at once.
expect(small_b.array_of_class_a_instances).to all(receive(:foo).exactly(1).time)

Related

Is there to shorten `def self.method_name` and `def method_name` into one method?

So say I have this class:
class This
def a(that)
puts that
end
def self.b(that)
puts that
end
end
This.b("Hello!") # => passes
This.a("Hello!") # => fails
the_class = This.new()
the_class.b("Hello!") # => fails
the_class.a("Hello!") # => passes
Is there a way to shorten both of those methods into one method that is able to be called on a uninitialized object, AND is able to be called on an already initialized one, or will I always have to write a method like that twice?
You can extract the functionality into a module and both extend and include it.
module A
def a(that)
puts that
end
end
class This
include A # defines instance methods
extend A # defines class methods
end
This.a("foo") # => "foo"
This.new.a("foo") # => "foo"
Although I think it's more common to either include or extend and not both. The reason being that instance methods often depend on instance state, whereas class methods don't. If you have an instance This.new and want to call a class method, you can use .class i.e. This.new.class.a
The following bit of code uses some metaprogramming tricks to auto copy over any class methods to instances of that class.
module AutoAddMethods
def singleton_method_added(symbol)
define_method(symbol, method(symbol).to_proc)
end
end
class Foo
extend AutoAddMethods
#bar = 39
def initialize
#bar = 42
end
def test_one # Only added as an instance method.
puts "One #{#bar.inspect}"
end
def self.test_two # Added as both an instance and class method.
puts "Two #{#bar.inspect}"
end
end
i = Foo.new
i.test_one
i.test_two
Foo.test_two
And here is my test run output:
One 42
Two 39
Two 39
The test_two method is callable from both the class and its instances. It runs as if in the class.

Ruby - Singleton module with each. Returning a value gives an Enumerator. How can I get the value instead?

Example code:
module Creatures
class << self
def to_h
{
squirtle: {full_name: 'Squirtle T. Turtle'},
pikachu: {full_name: 'Pikachu B. Pikachu'}
}
end
def keys
to_h.keys
end
def collect
to_h.keys.collect
end
def each
to_h.keys.each
end
end
end
module CompanionHelper
def get_companion_creature_experience(companion_data)
Creatures.each do |creature|
return companion_data[creature]["#{creature}_experience".to_sym] if companion_data.has_key?(creature)
end
end
end
include CompanionHelper
companion_data = {squirtle: {squirtle_experience: 8000}}
get_companion_creature_experience(companion_data)
Forgive me if the example is contrived. The original code is from the insurance world but I can't copy and paste it :)
The crux of the problem is I want to use Creatures.each in another module, pass it a block, and have it work just like Creatures.keys.each would work (i.e. w/ the given example companion data I get 8000 for get_companion_creature_experience(companion_data).
Currently I get Enumerator instead.
Problem is that to_h.keys.each returns Enumerator which does not expect any arguments. Pass a block inside each since you want to use it:
def each &block
to_h.keys.each &block
end
Or you can yield it:
def each
to_h.keys.each do |k|
yield k
end
end

Yielding inside a block

I've been given this class which includes the Enumerable module:
class Team
include Enumerable # LOTS of functionality
attr_accessor :name, :players
def initialize (name)
#name = name
#players = []
end
def add_players (*players) # splat
#players += players
end
def to_s
"#{#name} team: #{#players.join(", ")}"
end
def each
#players.each { |player| yield player }
end
end
I know that yield is used to be able to call a block inside a method when a block is passed as a method argument. What then does the yield do inside this block?
Proper functioning of the included Enumerable interface's methods requires that the class implement an each method that yields successive values. The implementation in your sample code effectively delegates this logic to that of Array's each method (since #players is an Array).
Replacing yield with return would result in no values being provided, and bypassing any code block that was passed in.
You could experiment with the each implementation to verify this.

Easily create an Enumerator

When creating methods that yield, sometimes we want it to return an Enumerator if no block is given. The recommended way is basically return to_enum(:name_of_method, [args]) unless block_given?. However, it's a pain to have to type that for every method that does this. Ruby being ruby, I decided to create a make_enum method, similar to attr_accessor, which does this for me:
class Module # Put this in a mixin, but for the purposes of this experiment, it's in Module
def make_enum *args
args.each do |name|
old_method = instance_method(name)
define_method(name) do |*args, &block|
next to_enum(name, *args) unless block
old_method.bind(self).call(*args, &block)
end
end
end
end
Now I can use it like so:
class Test
def test
yield 1
yield 2
end
make_enum :test
end
t = Test.new
t.test { |n| puts n }
# 1
# 2
t.test.to_a #=> [1, 2]
And it works! But it doesn't work if make_enum is before the method definition.
How can I get this method to work before defining a method, so that the following works? Perhaps I need to make use of method_added?
class Test
make_enum :test
def test
yield 1
yield 2
end
end
I don't know if it's a bad idea for it to be before the method, but my reason for thinking that it would be nice to do that is that it better matches the way we use attr_accessor and the like.
Whereas attr_ methods create instance methods newly, your make_enum modifies an existing method, which is rather similar to protected, private, and public methods. Note that these visibility methods are used either in the form:
protected
def foo; ... end
or
protected def foo; ... end
or
def foo; ... end
protected :foo
The latter two ways are already available with your make_enum. Especially, the second form is already possible (which Stefan also notes in the comment). You can do:
make_enum def test; ... end
If you want to do the first form, you should try to implement that in your make_enum definition.

How do I "fake" C# style attributes in Ruby?

EDIT: I slightly changed the spec, to better match what I imagined this to do.
Well, I don't really want to fake C# attributes, I want to one-up-them and support AOP as well.
Given the program:
class Object
def Object.profile
# magic code here
end
end
class Foo
# This is the fake attribute, it profiles a single method.
profile
def bar(b)
puts b
end
def barbar(b)
puts(b)
end
comment("this really should be fixed")
def snafu(b)
end
end
Foo.new.bar("test")
Foo.new.barbar("test")
puts Foo.get_comment(:snafu)
Desired output:
Foo.bar was called with param: b = "test"
test
Foo.bar call finished, duration was 1ms
test
This really should be fixed
Is there any way to achieve this?
I have a somewhat different approach:
class Object
def self.profile(method_name)
return_value = nil
time = Benchmark.measure do
return_value = yield
end
puts "#{method_name} finished in #{time.real}"
return_value
end
end
require "benchmark"
module Profiler
def method_added(name)
profile_method(name) if #method_profiled
super
end
def profile_method(method_name)
#method_profiled = nil
alias_method "unprofiled_#{method_name}", method_name
class_eval <<-ruby_eval
def #{method_name}(*args, &blk)
name = "\#{self.class}##{method_name}"
msg = "\#{name} was called with \#{args.inspect}"
msg << " and a block" if block_given?
puts msg
Object.profile(name) { unprofiled_#{method_name}(*args, &blk) }
end
ruby_eval
end
def profile
#method_profiled = true
end
end
module Comment
def method_added(name)
comment_method(name) if #method_commented
super
end
def comment_method(method_name)
comment = #method_commented
#method_commented = nil
alias_method "uncommented_#{method_name}", method_name
class_eval <<-ruby_eval
def #{method_name}(*args, &blk)
puts #{comment.inspect}
uncommented_#{method_name}(*args, &blk)
end
ruby_eval
end
def comment(text)
#method_commented = text
end
end
class Foo
extend Profiler
extend Comment
# This is the fake attribute, it profiles a single method.
profile
def bar(b)
puts b
end
def barbar(b)
puts(b)
end
comment("this really should be fixed")
def snafu(b)
end
end
A few points about this solution:
I provided the additional methods via modules which could be extended into new classes as needed. This avoids polluting the global namespace for all modules.
I avoided using alias_method, since module includes allow AOP-style extensions (in this case, for method_added) without the need for aliasing.
I chose to use class_eval rather than define_method to define the new method in order to be able to support methods that take blocks. This also necessitated the use of alias_method.
Because I chose to support blocks, I also added a bit of text to the output in case the method takes a block.
There are ways to get the actual parameter names, which would be closer to your original output, but they don't really fit in a response here. You can check out merb-action-args, where we wrote some code that required getting the actual parameter names. It works in JRuby, Ruby 1.8.x, Ruby 1.9.1 (with a gem), and Ruby 1.9 trunk (natively).
The basic technique here is to store a class instance variable when profile or comment is called, which is then applied when a method is added. As in the previous solution, the method_added hook is used to track when the new method is added, but instead of removing the hook each time, the hook checks for an instance variable. The instance variable is removed after the AOP is applied, so it only applies once. If this same technique was used multiple time, it could be further abstracted.
In general, I tried to stick as close to your "spec" as possible, which is why I included the Object.profile snippet instead of implementing it inline.
Great question. This is my quick attempt at an implementation (I did not try to optimise the code). I took the liberty of adding the profile method to the
Module class. In this way it will be available in every class and module definition. It would be even better
to extract it into a module and mix it into the class Module whenever you need it.
I also didn't know if the point was to make the profile method behave like Ruby's public/protected/private keywords,
but I implemented it like that anyway. All methods defined after calling profile are profiled, until noprofile is called.
class Module
def profile
require "benchmark"
#profiled_methods ||= []
class << self
# Save any original method_added callback.
alias_method :__unprofiling_method_added, :method_added
# Create new callback.
def method_added(method)
# Possible infinite loop if we do not check if we already replaced this method.
unless #profiled_methods.include?(method)
#profiled_methods << method
unbound_method = instance_method(method)
define_method(method) do |*args|
puts "#{self.class}##{method} was called with params #{args.join(", ")}"
bench = Benchmark.measure do
unbound_method.bind(self).call(*args)
end
puts "#{self.class}##{method} finished in %.5fs" % bench.real
end
# Call the original callback too.
__unprofiling_method_added(method)
end
end
end
end
def noprofile # What's the opposite of profile?
class << self
# Remove profiling callback and restore previous one.
alias_method :method_added, :__unprofiling_method_added
end
end
end
You can now use it as follows:
class Foo
def self.method_added(method) # This still works.
puts "Method '#{method}' has been added to '#{self}'."
end
profile
def foo(arg1, arg2, arg3 = nil)
puts "> body of foo"
sleep 1
end
def bar(arg)
puts "> body of bar"
end
noprofile
def baz(arg)
puts "> body of baz"
end
end
Call the methods as you would normally:
foo = Foo.new
foo.foo(1, 2, 3)
foo.bar(2)
foo.baz(3)
And get benchmarked output (and the result of the original method_added callback just to show that it still works):
Method 'foo' has been added to 'Foo'.
Method 'bar' has been added to 'Foo'.
Method 'baz' has been added to 'Foo'.
Foo#foo was called with params 1, 2, 3
> body of foo
Foo#foo finished in 1.00018s
Foo#bar was called with params 2
> body of bar
Foo#bar finished in 0.00016s
> body of baz
One thing to note is that it is impossible to dynamically get the name of the arguments with Ruby meta-programming.
You'd have to parse the original Ruby file, which is certainly possible but a little more complex. See the parse_tree and ruby_parser
gems for details.
A fun improvement would be to be able to define this kind of behaviour with a class method in the Module class. It would be cool to be able to do something like:
class Module
method_wrapper :profile do |*arguments|
# Do something before calling method.
yield *arguments # Call original method.
# Do something afterwards.
end
end
I'll leave this meta-meta-programming exercise for another time. :-)

Resources