Module & Mixin testing strategy

Hi,

Just wondered what people thoughts are to testing module’s to be
included in mixin’s? Seems to me there are two main approaches:-

  1. Test the behavior in a mixin object that includes the module because
    its the behavior of the object thats important not the code structure.

  2. Test the module in isolation as it potentially code be included
    anywhere.

If the best approach is 2 - to test the module in isolation and the
module uses instance variables or methods from the object its being
mixed with then we would need to create a test object in the rspec test
that included the module and defined the required instance variables and
methods. Does this lead to 1 being the best approach as we are not then
forced to mock up a mixin just to test the module?

The question came about because I recently had to get an untested rails
module under test that was included in a number of controllers and
depended on ‘request’ and ‘response’. I was then faced with either
testing one of the controllers that included that module but also added
further complexity or defining a new thin controller used solely for
testing the module within the spec file.

Interested to know your thoughts!

Victor

On 14 Aug 2010, at 11:34, Mike Howson wrote:

Just wondered what people thoughts are to testing module’s to be
included in mixin’s? Seems to me there are two main approaches:-

Hi Mike

I’ve been doing a lot of this sort of coding lately, as I’ve been
extracting duplicated code into a mini-framework based on modules.

  1. Test the behavior in a mixin object that includes the module because
    its the behavior of the object thats important not the code structure.

  2. Test the module in isolation as it potentially code be included
    anywhere.

I’m not sure I know how option 2 is even possible, unless your module is
all module methods, as you can’t call instance methods on a module
directly.

However, it’s easy to do this in RSpec with some Ruby meta-magic:

module MyModule
def foo
“bar”
end
end

describe MyModule do
let(:class_with_my_module) {
Class.new do
include MyModule
end
}

subject { class_with_my_module.new }

its(:foo) { should eq bar }

end

If the best approach is 2 - to test the module in isolation and the
module uses instance variables or methods from the object its being
mixed with then we would need to create a test object in the rspec test
that included the module and defined the required instance variables and
methods. Does this lead to 1 being the best approach as we are not then
forced to mock up a mixin just to test the module?

I’m not 100% sure but I think the snippet above is an implementation
of what you describe here. Please correct me if I misunderstood.

The question came about because I recently had to get an untested rails
module under test that was included in a number of controllers and
depended on ‘request’ and ‘response’. I was then faced with either
testing one of the controllers that included that module but also added
further complexity or defining a new thin controller used solely for
testing the module within the spec file.

In this case, you may be able to get some mileage with the above code,
but using Class.new(ActionController::Base).

You can test individual objects that include your module with shared
examples, for example:

module Fooable
def foo
“bar”
end
end

class Baz
include Fooable

# Oops - this is overriding Fooable#foo
def foo
  "quux"
end

end

shared_examples_for “a Fooable object” do
# Optional
before(:each) do
unless respond_to?(:fooable)
raise “You must provide instance method fooable”
end
end

it "should have a foo of 'bar'" do
  fooable.foo.should eq "bar"
end

end

describe Baz do
subject { Baz.new }
it_should_behave_like “a Fooable object” do
let(:fooable) { subject }
end
end

My recommendation at the moment is to make the shared examples work
fully-integrated (ie, no mocks). I’ve run into issue where shared
examples rely on mocks, which I haven’t solved yet (at least not in my
code - it’s my next TODO).

Currently I’m doing both the above. The isolated module spec proves the
module enchants objects with the correct behaviour, the shared examples
double-check that you haven’t broken that behaviour in concrete classes.

See also the recent thread “Evaluating shared example customisation
block before shared block” from 30th July onwards (it goes on to talk
about passing parameters to shared example groups, which is possible in
RSpec-2 master).

HTH

Ash


http://www.patchspace.co.uk/
http://www.linkedin.com/in/ashleymoran

On Aug 14, 2010, at 9:26 AM, Ashley M. wrote:

module MyModule
}

subject { class_with_my_module.new }

its(:foo) { should eq bar }
end

Or:

describe M do
it “does something” do
host = Object.new.extend(M)
host.some_method_defined_in_m.should do_something
end
end

I think either approach satisfies “test the module in isolation”, even
though it’s not in isolation from the behaviour of Object.

On Aug 14, 2010, at 5:34 AM, Mike Howson wrote:

Hi,

Just wondered what people thoughts are to testing module’s to be
included in mixin’s? Seems to me there are two main approaches:-

  1. Test the behavior in a mixin object that includes the module because
    its the behavior of the object thats important not the code structure.

  2. Test the module in isolation as it potentially code be included
    anywhere.

  1. All of the above, and then some …

I need to blog this, which I’ll do later, but here is the short version:

Consider this structure:

module M; end
class C
include M
end

We specify responsibilities of objects from the perspective of their
consumers. If module M is included in class C, consumers of class C have
no reason to know that module M is involved. They just care about the
behaviour. Same is true of classes A, B, and C, if they each include
module M. Keeping in mind that each host class/object (classes and
modules that include or extend M) can override any of the behaviour of
M, each host should therefore be specified independently.

Additionally, if module M enforces some rule, like host objects (i.e.
classes and modules that include or extend M) must implement method F,
then that responsibility belongs to M, and should be specified in the
context of M, not any of its host classes/objects.

So we’re interested in specifying two things:
a. the behaviour of each class/object that mixes in M in response to
events triggered by their consumers
b. the behaviour of M in response to being mixed in

For specifying the behaviour of M in response to being mixed in, I typically mix M into anonymous classes and objects and specify what happens. Brief example:

describe M do
it “requires host object to provide a foo method” do
host = Object.new
expect do
host.extend(M)
end.to raise_error(/Objects which extend M must provide a foo
method/)
end
end

For specifying the behaviour of host classes/objects, I’ve used a
combination of shared example groups and custom macros in the past, but
I don’t think the macros will be necessary any longer. Thanks to some
lively discussion [1-5], and code from Wincent C., Ashley M.
and Myron Marsten, shared example groups just got awesome! They can
now be parameterized and/or customized in three different ways. The
biggest change came from having it_should_behave_like (and its new
alias, it_behaves_like), generate a nested example group instead of
mixing a module directly into the host group. This means that these two
are equivalent:

shared_examples_for M
it “does something” do
# …
end
end

describe C do
it_behaves_like M
end

describe C do
context “behaves like M” do
it “does something” do
# …
end
end
end

In rspec-1, shared groups are modules that get mixed into the host
group, which means material defined in the shared group can impact the
host group in surprising ways. With this new structure in rspec-2, the
nested group is a completely separate group, and combination of sharing
behaviour (through inheritance) and isolating behaviour (through
encapsulation) provides power we never had before.

Here are the techniques for customizing shared groups:

Parameterization

describe Host do
it_should_behave_like M, Host.new
end

Here, the result of Host.new is passed to the shared group as a block
parameter, making that value available at the group level (each example
group is a class), and the instance level (each example runs in an
instance of that class). So …

shared_examples_for M do |host|
it “can access #{host} in the docstring” do
host.do_something # it can access the host in the example
end
end

Methods defined in host group

describe Host do
let(:foo) { Host.new }
it_should_behave_like M
end

In this case, the foo method defined by let is inherited by the
generated nested example group. Inherited methods like this are only
available in the scope in which they are defined, so foo would be
available at the instance level (i.e. in examples). If foo was defined
as a class method, then it would be available at the class level in the
nested group as well.

Methods defined in an extension block

describe Host do
it_should_behave_like M do
let(:foo) { Host.new }
end
end

In this case, the block passed to it_should_behave_like is eval’d after
the shared group is eval’d.

The combo of the extension block and inherited methods allows us to
define groups that programmatically enforce rules for the host groups.
For example:

shared_examples_for M do
unless respond_to?(:foo)
raise “Groups that include shared examples for M must provide a foo
method”
end
end

This means that library authors can now ship shared groups that will
instruct end users how to use them. Awesome!!!

I’ll amend and refine this in a blog post sometime soon, but hopefully
this is a helpful overview.

Cheers,
David

[1] Allow it_should_behave_like to take a block that can define helper_methods, hooks, etc. · Issue #71 · rspec/rspec-core · GitHub
[2] Can we get an alias for it_should_behave_like? · Issue #74 · rspec/rspec-core · GitHub
[3]
http://groups.google.com/group/rspec/browse_thread/thread/f5620df1c42874bf#
[4]
http://groups.google.com/group/rspec/browse_thread/thread/16d553ee2e51ccbd#
[5]
http://groups.google.com/group/rspec/browse_thread/thread/a23d5fb84a31f11e#

On 14 Aug 2010, at 15:50, David C. wrote:

I need to blog this, which I’ll do later, but here is the short version:

You could pretty much copy and paste that “short version” into a chapter
of the RSpec Book, never mind a blog post :wink:


http://www.patchspace.co.uk/
http://www.linkedin.com/in/ashleymoran

Wow - thanks very much Ashley & David for that very comprehensive
response, you’ve answered my question and more!

Cheers, Mike.