Testing modules in RSpec

77,414

Solution 1

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

Solution 2

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()?

Solution 3

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

Solution 4

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?

Solution 5

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.

Share:
77,414

Related videos on Youtube

Andrius
Author by

Andrius

Updated on July 21, 2021

Comments

  • Andrius
    Andrius almost 3 years

    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?

  • Andrius
    Andrius over 14 years
    What if my module depends on class having certain attributes and behavior?
  • captainpete
    captainpete almost 12 years
    Nice. This helped me avoid all sorts of issues with class ivars spanning tests. Gave the classes names by assigning to constants.
  • SooDesuNe
    SooDesuNe almost 12 years
    By "cooler" what @gri0n means is: that let is better than assigning an instance variable as the dummy class in a before(:each) (or better before(:all)). IMO, the best reason is that you'll get a NameError, instead of nil if you fat finger it. Have a look at this SO on when to use let
  • Grant Birchmeier
    Grant Birchmeier over 11 years
    Any reason you didn't include Say inside of the DummyClass declaration instead of calling extend?
  • pduey
    pduey over 11 years
    I like this. It works for the methods defined in the module. But, one of my modules has some methods which act on containing class attributes. I tried adding those in my dynamically defined proxy class with attr_accessor, but they don't work in rspec. Oddly, they do work in the console.
  • Jared
    Jared over 11 years
  • Hedgehog
    Hedgehog over 11 years
    grant-birchmeier, he's extending into the instance of the class, i.e. after new has been called. If you were doing this before new is called then you are right you would use include
  • ian
    ian about 11 years
    I like this, it's so straightforward.
  • Tim Harper
    Tim Harper about 11 years
    I edited the code to be more concise. @dummy_class = Class.new { extend Say } is all you need to test a module. I suspect people will prefer that as we developers often do not like to type more than necessary.
  • lulalala
    lulalala almost 11 years
    Is Siliconseller::CodeGenerator the module?
  • lulalala
    lulalala almost 11 years
    @TimHarper Tried but instance methods became class methods. Thoughts?
  • Tim Harper
    Tim Harper over 10 years
    @lulalala that's right, if you want to test a module of functions (that doesn't depend on some some state for the object into which they are mixed) then having them be class methods is ideal. In fact, it may be better to use @helper = Module.new { extend Say }, since you'll have no use for instantiating @helper.
  • lulalala
    lulalala over 10 years
    I am getting superclass must be a Class (Module given) error.
  • Timo
    Timo over 10 years
    Why would you define the DummyClass constant? Why not just @dummy_class = Class.new? Now your polluting your test environment with an unnecessary class definition. This DummyClass is defined for every and each one of your specs and in the next spec where you decide to use the same approach and reopen the DummyClass definition it might already contain something (though in this trivial example the definition is strictly empty, in real life use cases it's likely that something gets added at some point and then this approach becomes dangerous.)
  • Timo
    Timo over 10 years
    @lulalala No, it's a super class: ruby-doc.org/core-2.0.0/Class.html#method-c-new To test modules do something like this: let(:dummy_class) { Class.new { include ModuleToBeTested } }
  • Automatico
    Automatico over 10 years
    Can you somehow access this dummy class in other let statements?
  • Automatico
    Automatico over 10 years
    Way rad. I usually do: let(:class_instance) { (Class.new { include Super::Duper::Module }).new }, that way I get the instance variable that is most often used for testing any way.
  • Automatico
    Automatico over 10 years
    Might mess up the rspec. I think using the let method described by @metakungfu is better.
  • Frank C. Schuetz
    Frank C. Schuetz about 10 years
    @Cort3z You definitely need to make sure that method names don't collide. I'm using this approach only when things are really simple.
  • valk
    valk over 9 years
    For some reason only subject { dummy_class.new } is working. The case with subject { dummy_class } isn't working for me.
  • Christos Hrousis
    Christos Hrousis over 9 years
    Caveat: let can only be used in describe block, so if you looked at Carmen's answer and try to paste the following answer in your before each block it won't work (this wasn't clear for me)
  • user115014
    user115014 over 7 years
    using include does not work for me but extend does let(:dummy_class) { Class.new { extend ModuleToBeTested } }
  • Richard-Degenne
    Richard-Degenne over 6 years
    Even radder: subject(:instance) { Class.new.include(described_class).new }
  • David Hempy
    David Hempy over 5 years
    Just to be a fuss-budget...I used your model (Thanks!) but instead of calling it :dummy_class, I called it :objekt. since we're creating an instance, not a class. YMMW.
  • roxxypoxxy
    roxxypoxxy over 5 years
    This messed up my test suite due to name collision.
  • Allison
    Allison almost 5 years
    As others have said, recommend using subject over let here because you're using a mock instance of the thing being tested.
  • Allison
    Allison over 4 years
    I wasn't the one who downvoted you, but I suggest replacing your two LETs with subject(:module_to_test_instance) { Class.new.include(described_class) }. Otherwise I don't really see anything wrong with your answer.
  • RonLugge
    RonLugge over 3 years
    What if you needed the parent class to implement methods? In my use case, I'm wanting to test the modules in isolation of the including class because I'm trying to isolate out the Faraday calls out of the tests, but another example might be two classes with different data sources.