Test obj.extend(Foo) with RSpec doubles - ruby

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(...) }

Related

Module classes not visible in Minitest describe

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")

Ruby + Rspec + OpenStruct weird behavior

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

How to test a method call in a constructor with rspec

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.

How do I write an RSpec test to unit-test this interesting metaprogramming code?

Here's some simple code that, for each argument specified, will add specific get/set methods named after that argument. If you write attr_option :foo, :bar, then you will see #foo/foo= and #bar/bar= instance methods on Config:
module Configurator
class Config
def initialize()
#options = {}
end
def self.attr_option(*args)
args.each do |a|
if not self.method_defined?(a)
define_method "#{a}" do
#options[:"#{a}"] ||= {}
end
define_method "#{a}=" do |v|
#options[:"#{a}"] = v
end
else
throw Exception.new("already have attr_option for #{a}")
end
end
end
end
end
So far, so good. I want to write some RSpec tests to verify this code is actually doing what it's supposed to. But there's a problem! If I invoke attr_option :foo in one of the test methods, that method is now forever defined in Config. So a subsequent test will fail when it shouldn't, because foo is already defined:
it "should support a specified option" do
c = Configurator::Config
c.attr_option :foo
# ...
end
it "should support multiple options" do
c = Configurator::Config
c.attr_option :foo, :bar, :baz # Error! :foo already defined
# by a previous test.
# ...
end
Is there a way I can give each test an anonymous "clone" of the Config class which is independent of the others?
One very simple way to "clone" your Config class is to simply subclass it with an anonymous class:
c = Class.new Configurator::Config
c.attr_option :foo
d = Class.new Configurator::Config
d.attr_option :foo, :bar
This runs for me without error. This works because all instance variables and methods that get set are tied to the anonymous class instead of Configurator::Config.
The syntax Class.new Foo creates an anonymous class with Foo as a superclass.
Also, throwing an Exception in Ruby is incorrect; Exceptions are raised. throw is meant to be used like a goto, such as to break out of multiple nests. Read this Programming Ruby section for a good explanation on the differences.
As another style nitpick, try not to use if not ... in Ruby. That's what unless is for. But unless-else is poor style as well. I'd rewrite the inside of your args.each block as:
raise "already have attr_option for #{a}" if self.method_defined?(a)
define_method "#{a}" do
#options[:"#{a}"] ||= {}
end
define_method "#{a}=" do |v|
#options[:"#{a}"] = v
end

Testing modules in RSpec

What are the best practices on testing modules in RSpec? I have some modules that get included in few models and for now I simply have duplicate tests for each model (with few differences). Is there a way to DRY it up?
The rad way =>
let(:dummy_class) { Class.new { include ModuleToBeTested } }
Alternatively you can extend the test class with your module:
let(:dummy_class) { Class.new { extend ModuleToBeTested } }
Using 'let' is better than using an instance variable to define the dummy class in the before(:each)
When to use RSpec let()?
What mike said. Here's a trivial example:
module code...
module Say
def hello
"hello"
end
end
spec fragment...
class DummyClass
end
before(:each) do
#dummy_class = DummyClass.new
#dummy_class.extend(Say)
end
it "get hello string" do
expect(#dummy_class.hello).to eq "hello"
end
For modules that can be tested in isolation or by mocking the class, I like something along the lines of:
module:
module MyModule
def hallo
"hallo"
end
end
spec:
describe MyModule do
include MyModule
it { hallo.should == "hallo" }
end
It might seem wrong to hijack nested example groups, but I like the terseness. Any thoughts?
I found a better solution in rspec homepage. Apparently it supports shared example groups. From https://www.relishapp.com/rspec/rspec-core/v/2-13/docs/example-groups/shared-examples!
Shared Example Groups
You can create shared example groups
and include those groups into other
groups.
Suppose you have some behavior that
applies to all editions of your
product, both large and small.
First, factor out the “shared”
behavior:
shared_examples_for "all editions" do
it "should behave like all editions" do
end
end
then when you need define the behavior
for the Large and Small editions,
reference the shared behavior using
the it_should_behave_like() method.
describe "SmallEdition" do
it_should_behave_like "all editions"
it "should also behave like a small edition" do
end
end
Off the top of my head, could you create a dummy class in your test script and include the module into that? Then test that the dummy class has the behaviour in the way you'd expect.
EDIT: If, as pointed out in the comments, the module expects some behaviours to be present in the class into which it's mixed, then I'd try to implement dummies of those behaviours. Just enough to make the module happy to perform its duties.
That said, I'd be a little nervous about my design when a module expects a whole lot from its host (do we say "host"?) class - If I don't already inherit from a base class or can't inject the new functionality into the inheritance tree then I think I'd be trying to minimise any such expectations that a module might have. My concern being that my design would start to develop some areas of unpleasant inflexibility.
The accepted answer is the right answer I think, however I wanted to add an example how to use rpsecs shared_examples_for and it_behaves_like methods. I mention few tricks in the code snippet but for more info see this relishapp-rspec-guide.
With this you can test your module in any of the classes which include it. So you really are testing what you use in your application.
Let's see an example:
# Lets assume a Movable module
module Movable
def self.movable_class?
true
end
def has_feets?
true
end
end
# Include Movable into Person and Animal
class Person < ActiveRecord::Base
include Movable
end
class Animal < ActiveRecord::Base
include Movable
end
Now lets create spec for our module: movable_spec.rb
shared_examples_for Movable do
context 'with an instance' do
before(:each) do
# described_class points on the class, if you need an instance of it:
#obj = described_class.new
# or you can use a parameter see below Animal test
#obj = obj if obj.present?
end
it 'should have feets' do
#obj.has_feets?.should be_true
end
end
context 'class methods' do
it 'should be a movable class' do
described_class.movable_class?.should be_true
end
end
end
# Now list every model in your app to test them properly
describe Person do
it_behaves_like Movable
end
describe Animal do
it_behaves_like Movable do
let(:obj) { Animal.new({ :name => 'capybara' }) }
end
end
To test your module, use:
describe MyCoolModule do
subject(:my_instance) { Class.new.extend(described_class) }
# examples
end
To DRY up some things you use across multiple specs, you can use a shared context:
RSpec.shared_context 'some shared context' do
let(:reused_thing) { create :the_thing }
let(:reused_other_thing) { create :the_thing }
shared_examples_for 'the stuff' do
it { ... }
it { ... }
end
end
require 'some_shared_context'
describe MyCoolClass do
include_context 'some shared context'
it_behaves_like 'the stuff'
it_behaves_like 'the stuff' do
let(:reused_thing) { create :overrides_the_thing_in_shared_context }
end
end
Resources:
Shared Examples
Shared Context
BetterSpecs
my recent work, using as little hard-wiring as possible
require 'spec_helper'
describe Module::UnderTest do
subject {Object.new.extend(described_class)}
context '.module_method' do
it {is_expected.to respond_to(:module_method)}
# etc etc
end
end
I wish
subject {Class.new{include described_class}.new}
worked, but it doesn't (as at Ruby MRI 2.2.3 and RSpec::Core 3.3.0)
Failure/Error: subject {Class.new{include described_class}.new}
NameError:
undefined local variable or method `described_class' for #<Class:0x000000063a6708>
Obviously described_class isn't visible in that scope.
What about:
describe MyModule do
subject { Object.new.extend(MyModule) }
it "does stuff" do
expect(subject.does_stuff?).to be_true
end
end
I would suggest that for larger and much used modules one should opt for the "Shared Example Groups" as suggested by #Andrius here. For simple stuff for which you don't want to go through the trouble of having multiple files etc. here's how to ensure maximum control over the visibility of your dummy stuff (tested with rspec 2.14.6, just copy and paste the code into a spec file and run it):
module YourCoolModule
def your_cool_module_method
end
end
describe YourCoolModule do
context "cntxt1" do
let(:dummy_class) do
Class.new do
include YourCoolModule
#Say, how your module works might depend on the return value of to_s for
#the extending instances and you want to test this. You could of course
#just mock/stub, but since you so conveniently have the class def here
#you might be tempted to use it?
def to_s
"dummy"
end
#In case your module would happen to depend on the class having a name
#you can simulate that behaviour easily.
def self.name
"DummyClass"
end
end
end
context "instances" do
subject { dummy_class.new }
it { subject.should be_an_instance_of(dummy_class) }
it { should respond_to(:your_cool_module_method)}
it { should be_a(YourCoolModule) }
its (:to_s) { should eq("dummy") }
end
context "classes" do
subject { dummy_class }
it { should be_an_instance_of(Class) }
it { defined?(DummyClass).should be_nil }
its (:name) { should eq("DummyClass") }
end
end
context "cntxt2" do
it "should not be possible to access let methods from anohter context" do
defined?(dummy_class).should be_nil
end
end
it "should not be possible to access let methods from a child context" do
defined?(dummy_class).should be_nil
end
end
#You could also try to benefit from implicit subject using the descbie
#method in conjunction with local variables. You may want to scope your local
#variables. You can't use context here, because that can only be done inside
#a describe block, however you can use Porc.new and call it immediately or a
#describe blocks inside a describe block.
#Proc.new do
describe "YourCoolModule" do #But you mustn't refer to the module by the
#constant itself, because if you do, it seems you can't reset what your
#describing in inner scopes, so don't forget the quotes.
dummy_class = Class.new { include YourCoolModule }
#Now we can benefit from the implicit subject (being an instance of the
#class whenever we are describing a class) and just..
describe dummy_class do
it { should respond_to(:your_cool_module_method) }
it { should_not be_an_instance_of(Class) }
it { should be_an_instance_of(dummy_class) }
it { should be_a(YourCoolModule) }
end
describe Object do
it { should_not respond_to(:your_cool_module_method) }
it { should_not be_an_instance_of(Class) }
it { should_not be_an_instance_of(dummy_class) }
it { should be_an_instance_of(Object) }
it { should_not be_a(YourCoolModule) }
end
#end.call
end
#In this simple case there's necessarily no need for a variable at all..
describe Class.new { include YourCoolModule } do
it { should respond_to(:your_cool_module_method) }
it { should_not be_a(Class) }
it { should be_a(YourCoolModule) }
end
describe "dummy_class not defined" do
it { defined?(dummy_class).should be_nil }
end
You can also use the helper type
# api_helper.rb
module Api
def my_meth
10
end
end
# spec/api_spec.rb
require "api_helper"
RSpec.describe Api, :type => :helper do
describe "#my_meth" do
it { expect( helper.my_meth ).to eq 10 }
end
end
Here's the documentation: https://www.relishapp.com/rspec/rspec-rails/v/3-3/docs/helper-specs/helper-spec
you need to simply include your module to your spec file
mudule Test
module MyModule
def test
'test'
end
end
end
in your spec file
RSpec.describe Test::MyModule do
include Test::MyModule #you can call directly the method *test*
it 'returns test' do
expect(test).to eql('test')
end
end
One possible solution for testing module method which are independent on class that will include them
module moduleToTest
def method_to_test
'value'
end
end
And spec for it
describe moduleToTest do
let(:dummy_class) { Class.new { include moduleToTest } }
let(:subject) { dummy_class.new }
describe '#method_to_test' do
it 'returns value' do
expect(subject.method_to_test).to eq('value')
end
end
end
And if you want to DRY test them, then shared_examples is good approach
This is a recurrent pattern since you're going to need to test more than one module. For that reason, this is more than desirable to create a helper for this.
I found this post that explains how to do it but I'm coping here since the site might be taken down at some point.
This is to avoid the object instances do not implement the instance method: :whatever error you get when trying to allow methods on dummy class.
Code:
In spec/support/helpers/dummy_class_helpers.rb
module DummyClassHelpers
def dummy_class(name, &block)
let(name.to_s.underscore) do
klass = Class.new(&block)
self.class.const_set name.to_s.classify, klass
end
end
end
In spec/spec_helper.rb
# skip this if you want to manually require
Dir[File.expand_path("../support/**/*.rb", __FILE__)].each {|f| require f}
RSpec.configure do |config|
config.extend DummyClassHelpers
end
In your specs:
require 'spec_helper'
RSpec.shared_examples "JsonSerializerConcern" do
dummy_class(:dummy)
dummy_class(:dummy_serializer) do
def self.represent(object)
end
end
describe "#serialize_collection" do
it "wraps a record in a serializer" do
expect(dummy_serializer).to receive(:represent).with(an_instance_of(dummy)).exactly(3).times
subject.serialize_collection [dummy.new, dummy.new, dummy.new]
end
end
end

Resources