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.

3. All of the above, and then some ...

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

<high-level>
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
</high-level> 

<in-practice>
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 Colaiuta, Ashley Moran 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!!!!!!!
</in-practice>

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

Cheers,
David

[1] http://github.com/rspec/rspec-core/issues/issue/71
[2] http://github.com/rspec/rspec-core/issues/issue/74
[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#

> 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
> -- 
> Posted via http://www.ruby-forum.com/.
> _______________________________________________
> rspec-users mailing list
> rspec-users@rubyforge.org
> http://rubyforge.org/mailman/listinfo/rspec-users

_______________________________________________
rspec-users mailing list
rspec-users@rubyforge.org
http://rubyforge.org/mailman/listinfo/rspec-users

Reply via email to