So i'm experiencing this weird behavior while testing a ruby class. I'm using rspec 3 to test it by the way.
Class Foo has a method 'fetch_object' which calls the 'find' method from class Bar to retrieve an object and than calls the method 'fail' from the fetched object.
The so called weird behavior happens when i expect to receive the method 'fail' once and receive none but if I change the method name for 'faill' it works like a charm :S
here is the drama:
require 'ostruct'
class Foo
def fetch_object
foobar = Bar.find
foobar.fail
end
end
class Bar
def self.find
OpenStruct.new(name: 'Foo Bar')
end
end
describe Foo do
subject { Foo.new }
let(:foo) { OpenStruct.new() }
before do
expect(Bar).to receive(:find).and_return(foo)
end
it 'fetch object with name' do
expect(foo).to receive(:fail)
subject.fetch_object
end
end
I suspect it's because you are setting an expectation on object, which behaviour depends on method_missing (OpenStruct).
For that reason I wouldn't want it as a mock, I would use regular mock (and spec will pass):
let(:foo) { double('foobar') }
You are testing here, if returned object (result of Bar.find) will receive an expected message, without going into implementation details.
Setting expectations on Dynamic classes like ostruct may lead to strange results. It seems that at some point a Kernel#fail method is invoked, thus, changing a name to faill or any other that is not already "taken" by Kernel will make it work.
Other solution would be monkeypatching OpenStruct to avoid method_missing beeing called:
class OpenStruct
def fail
true
end
end
class Foo
def fetch_object
foobar = Bar.find
foobar.fail
end
end
class Bar
def self.find
OpenStruct.new(name: 'Foo Bar')
end
end
describe Foo do
subject { Foo.new }
let(:foo) { OpenStruct.new }
before do
expect(Bar).to receive(:find).and_return(foo)
end
it 'fetch object with name' do
expect(foo).to receive(:fail)
subject.fetch_object
end
end
But I don't know why would you want to do that ;)
More info: Doubles and dynamic classess
Related
When I want to include a module into a Minitest/spec test, I can access the functions from the module, but not the classes defined in it. Example:
module Foo
def do_stuff
end
class Bar
end
end
x=describe Foo do
include Foo
end
p x.constants # shows :Bar
describe Foo do
include Foo
it "foos" do
do_stuff # works
Bar.new # raises a NameError
end
end
Running this snippet gives me a "NameError: uninitialized constant Bar", however, the p x.constantsshows that Bar is defined. I looked into the Minitest source code for describe and it uses class_eval on the block in the context of some anonymous class. When I do that in the context of a normal class it works fine and I can access Bar. Why doesn't it work with describe/it or what do I have to do in order to access the classes directly?
EDIT:
Interestingly, if you call class_eval directly on some class the included class Bar can be found, e.g.
class Quux
def it_foos
do_stuff # works
Bar.new # does NOT raise a NameError
end
end
Quux.class_eval do
include Foo
end
Quux.new.it_foos
won't throw a NameError...
If you check the documentation for #class_eval (for example, https://ruby-doc.org/core-2.5.0/Module.html#method-i-class_eval) you will see the answer there: "Evaluates the string or block in the context of mod, except that when a block is given, constant/class variable lookup is not affected".
So, include within class_eval simply doesn't affect constants resolution.
As far as I understand from the short look at minitest's source code, describe internally creates a new anonymous class (let's name it C) and casts class_eval on it with the block you provide. During this call its create the respective test instance methods that are executed later. But include doesn't affect constants resolution for C, so Bar stays unknown.
There is an obvious (and quite ugly) solution - the following should work because you include Foo into outer context, so Bar goes into lexical scope accessible for describe:
include Foo
describe Foo do
it "foos" do
do_stuff
Bar.new
end
end
But tbh I'd avoid such code. Probably it's better to set up the class mock explicitly, smth like
module Foo
def do_stuff
"foo"
end
class Bar
def do_stuff
"bar"
end
end
end
...
describe Foo do
let(:cls) { Class.new }
before { cls.include(Foo) }
it "foos" do
assert cls.new.do_stuff == "foo"
end
it "bars" do
assert cls::Bar.new.do_stuff == "bar"
end
end
(but take pls the latter with a grain of salt - I almost never use Minitest so have no idea of its "common idioms")
Sample code:
class Foo
def initialize(abc)
#abc = abc
#bind = bar
end
def bar
SomeClass.new(#abc)
end
end
Now I want to stub bar using rspec and custom stub:
allow('Foo').to receive(:bar).and_return(FakeBar.new)
The issue is that the FakeBar.new has to be initialize with the same arguments :bar receives. Is it possible to get a copy of params passed to :bar at the time we are stubbing and reuse them in the stub class?
Not sure why you want to do what you're doing (probably there is a simpler way), but for what it's worth:
allow("Foo").to receive(:bar) { |arg1, arg2| FakeBar.new(arg1, arg2) }
RSpec docs, block stub implementation
RSpec doubles can not be changed by extending its instances.
Minimal example
Please note that the example described here is just a minimal example
to demonstrate the problem. The original classes are more complex and
the behavior that is specced (obj.extend(Something)) is needed and
can not be changed.
Spec
Let us have a look at the spec first.
In the following example you can see the spec how I'd like it to look like:
require 'spec_helper'
RSpec.describe Modifier do
subject(:modifier) { described_class.new(active) }
describe "#apply_to(obj)" do
subject(:obj) { instance_double(Foo, foo: "foo") }
before { modifier.apply_to(obj) }
context "when active" do
let(:active) { true }
its(:foo) { is_expected.to eq("extended-foo") } # NOTE: This one will fail
end
context "when not active" do
let(:active) { false }
its(:foo) { is_expected.to eq("foo") }
end
end
end
Unfortunately, this is not working :(
Failures:
1) Modifier#apply_to(obj) when active foo should eq "extended-foo"
Failure/Error: its(:foo) { is_expected.to eq("extended-foo") }
expected: "extended-foo"
got: "foo"
Foo
class Foo
attr_reader :foo
def initialize(foo)
#foo = foo
end
end
Modifier
This class will modify given objects by extending its instance methods.
class Modifier
def initialize(active)
#active = active
end
# NOTE: This is the interesting method
def apply_to(obj)
return unless active?
obj.extend(Extended) # NOTE: And this is the interesting LOC
end
def active?
#active
end
private
attr_reader :active
module Extended
def foo
"extended-#{super}"
end
end
end
Even more minimal example
The whole problem can be broken down to the following code snippet:
module Bar
def foo
"bar"
end
end
double = RSpec::Mocks::Double.new("Foo", foo: "foo")
obj = Object.new
double.extend(Bar)
obj.extend(Bar)
double.foo
# => "foo"
obj.foo
# => "bar"
Conclusion
RSpec doubles can not be changed by extending its instances, which is expected behavior?
If so, how can you create readable specs for the example described here?
Links
Repo to reproduce: Extend RSpec doubles (Example)
RSpec: Using an instance double
GitHub: RSpec Issue 1100
If the spec knows that it expexts an extendend object, why not expect the extend call itself?
expect(obj).to receive(:extend).and_return { class.new.extend(...) }
I would like to access a class' name in its superclass MySuperclass' self.inherited method. It works fine for concrete classes as defined by class Foo < MySuperclass; end but it fails when using anonymous classes. I tend to avoid creating (class-)constants in tests; I would like it to work with anonymous classes.
Given the following code:
class MySuperclass
def self.inherited(subclass)
super
# work with subclass' name
end
end
klass = Class.new(MySuperclass) do
def self.name
'FooBar'
end
end
klass#name will still be nil when MySuperclass.inherited is called as that will be before Class.new yields to its block and defines its methods.
I understand a class gets its name when it's assigned to a constant, but is there a way to set Class#name "early" without creating a constant?
I prepared a more verbose code example with failing tests to illustrate what's expected.
Probably #yield has taken place after the ::inherited is called, I saw the similar behaviour with class definition. However, you can avoid it by using ::klass singleton method instead of ::inherited callback.
def self.klass
#klass ||= (self.name || self.to_s).gsub(/Builder\z/, '')
end
I am trying to understand the benefit of being able to refer to an anonymous class by a name you have assigned to it after it has been created. I thought I might be able to move the conversation along by providing some code that you could look at and then tell us what you'd like to do differently:
class MySuperclass
def self.inherited(subclass)
# Create a class method for the subclass
subclass.instance_eval do
def sub_class() puts "sub_class here" end
end
# Create an instance method for the subclass
subclass.class_eval do
def sub_instance() puts "sub_instance here" end
end
end
end
klass = Class.new(MySuperclass) do
def self.name=(name)
#name = Object.const_set(name, self)
end
def self.name
#name
end
end
klass.sub_class #=> "sub_class here"
klass.new.sub_instance #=> "sub_instance here"
klass.name = 'Fido' #=> "Fido"
kn = klass.name #=> Fido
kn.sub_class #=> "sub_class here"
kn.new.sub_instance #=> "sub_instance here"
klass.name = 'Woof' #=> "Woof"
kn = klass.name #=> Fido (cannot change)
There is no way in pure Ruby to set a class name without assigning it to a constant.
If you're using MRI and want to write yourself a very small C extension, it would look something like this:
VALUE
force_class_name (VALUE klass, VALUE symbol_name)
{
rb_name_class(klass, SYM2ID(symbol_name));
return klass;
}
void
Init_my_extension ()
{
rb_define_method(rb_cClass, "force_class_name", force_class_name, 1);
}
This is a very heavy approach to the problem. Even if it works it won't be guaranteed to work across various versions of ruby, since it relies on the non-API C function rb_name_class. I'm also not sure what the behavior will be once Ruby gets around to running its own class-naming hooks afterward.
The code snippet for your use case would look like this:
require 'my_extension'
class MySuperclass
def self.inherited(subclass)
super
subclass.force_class_name(:FooBar)
# work with subclass' name
end
end
i have a constructor like this:
class Foo
def initialize(options)
#options = options
initialize_some_other_stuff
end
end
and want to test the call to initialize_some_other_stuff if a instantiate a new Foo object.
I found this question rspec: How to stub an instance method called by constructor? but the suggested solution to call Foo.any_instance(:initialize_some_other_stuff) does not work in my rspec version (2.5.0).
Can anyone help me to test this constructor call?
In you spec, you could have the following:
class Foo
attr_reader :initializer_called
def initialize_some_other_stuff
#initializer_called = true
end
end
foo = Foo.new
foo.initializer_called.should == true
If the constructor calls the initiaize_some_other_stuff method, foo.initializer_called should be true.
Here you go:
stub_model(Foo).should_receive(:some_method_call).with(optional_argument)
Since the initialize_some_other_stuff method is private to the class, you should not care if it executes or not. That said, if that method performs some expensive operation that you don't want your test waiting for, then it is quite okay to mock that operation.
So, if Foo looked like this:
class Foo
attr_reader :options, :other_stuff
def initialize(options)
#options = options
initialize_some_other_stuff
end
def initialize_some_other_stuff
#other_stuff = Bar.new.long_running_operation
end
end
Then you could mock out the call to Bar#long_running_operation like this:
describe Foo do
subject(:foo) { described_class.new(options) }
let(:options) { 'options' }
let(:bar) { instance_double(Bar, long_running_operation: 42) }
before do
allow(Bar).to receive(:new).and_return(bar)
foo
end
it 'initializes options' do
expect(foo.options).to eq(options)
end
it 'initializes other stuff' do
expect(foo.other_stuff).to eq(bar.long_running_operation)
end
end
Now, you're testing the assignments. But, you're not waiting on the expensive operation to complete.