Testing modules in RSpec
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)
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.
Related videos on Youtube
Andrius
Updated on July 21, 2021Comments
-
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 over 14 yearsWhat if my module depends on class having certain attributes and behavior?
-
captainpete almost 12 yearsNice. This helped me avoid all sorts of issues with class ivars spanning tests. Gave the classes names by assigning to constants.
-
SooDesuNe almost 12 yearsBy "cooler" what @gri0n means is: that
let
is better than assigning an instance variable as the dummy class in abefore(:each)
(or betterbefore(:all)
). IMO, the best reason is that you'll get aNameError
, instead ofnil
if you fat finger it. Have a look at this SO on when to use let -
Grant Birchmeier over 11 yearsAny reason you didn't
include Say
inside of the DummyClass declaration instead of callingextend
? -
pduey over 11 yearsI 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 over 11 years
-
Hedgehog over 11 yearsgrant-birchmeier, he's
extend
ing into the instance of the class, i.e. afternew
has been called. If you were doing this beforenew
is called then you are right you would useinclude
-
ian about 11 yearsI like this, it's so straightforward.
-
Tim Harper about 11 yearsI 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 almost 11 yearsIs
Siliconseller::CodeGenerator
the module? -
lulalala almost 11 years@TimHarper Tried but instance methods became class methods. Thoughts?
-
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 over 10 yearsI am getting
superclass must be a Class (Module given)
error. -
Timo over 10 yearsWhy 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 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 over 10 yearsCan you somehow access this dummy class in other
let
statements? -
Automatico over 10 yearsWay 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 over 10 yearsMight mess up the rspec. I think using the
let
method described by @metakungfu is better. -
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 over 9 yearsFor some reason only
subject { dummy_class.new }
is working. The case withsubject { dummy_class }
isn't working for me. -
Christos Hrousis over 9 yearsCaveat:
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 over 7 yearsusing
include
does not work for me butextend
doeslet(:dummy_class) { Class.new { extend ModuleToBeTested } }
-
Richard-Degenne over 6 yearsEven radder:
subject(:instance) { Class.new.include(described_class).new }
-
David Hempy over 5 yearsJust 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 over 5 yearsThis messed up my test suite due to name collision.
-
Allison almost 5 yearsAs others have said, recommend using
subject
overlet
here because you're using a mock instance of the thing being tested. -
Allison over 4 yearsI 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 over 3 yearsWhat 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.