Testing Ruby Mixins with Minitest in isolation
Table of Contents
Mixins in Ruby are a very powerful feature. But knowing how to test them sometimes is not so obvious, especially to beginners. I think that this comes from mixins’ nature - they get mixed into other classes. So, if you think that there is a lot to testing mixins and you easily get overwhelmed by it - take it easy, it’s not that hard.
Let’s see how easy it is to test mixins, with some help from the lovely Minitest.
Mixins #
When writing Ruby, we usually write two types of mixins. The first one is a coupled mixin, and the other, well.. uncoupled.
Uncoupled mixins #
Uncoupled mixins are the ones whose methods do not depend on the implementation of the class where they will get mixed in.
Quick example:
module Speedable
def speed
"This car runs super fast!"
end
end
class PetrolCar
include Speedable
def fuel
"Petrol"
end
end
class DieselCar
include Speedable
def fuel
"Diesel"
end
end
In irb
:
>> p = PetrolCar.new
=> #<PetrolCar:0x007fc332cc4be0>
>> p.speed
=> "This car runs super fast!"
>> p.fuel
=> "Petrol"
>> d = DieselCar.new
=> #<DieselCar:0x007fc332cae2f0>
>> d.speed
=> "This car runs super fast!"
>> d.fuel
=> "Diesel"
As you can see, the speed
method in the Speedable
mixins does not depend on
any other methods. In other words, it’s self-contained.
Testing uncoupled mixins #
When it comes to testing uncoupled mixins, it’s quite trivial. There are two main strategies that you can use, by extending the singleton class of an object or by using a dummy class.
Let’s see the first one.
class FastCarTest < Minitest::Test
def setup
@test_obj = Object.new
@test_obj.extend(Speedable)
end
def test_speed_reported
assert_equal "This car runs super fast!", @test_obj.speed
end
end
As you can see, we instantiate an object of the Object
class which is just an
empty, ordinary object that doesn’t do anything. Then, we extend the object
singleton class with the Speedable
module which will mix the speed
method
in. Then, in the test, we assert that the method will return the expected
output.
The second strategy is the “dummy class” strategy:
class DummyTestClass
include Speedable
end
class FastCarTest < Minitest::Test
def test_speed_reported
dummy = DummyTestClass.new
assert_equal "This car runs super fast!", dummy.speed
end
end
As you can see, we create just a dummy class, specific only for this test file.
Since the FastCar
mixin is mixed in, the DummyTestClass
will have the
speed
method as an instance method. Then, in the test, we just create a new
object from the dummy class and assert on the dummy.speed
method.
Coupled mixins #
Coupled mixins are the ones whose methods depend on the implementation of the class where they will be mixed in. Or, the oposite of uncoupled mixins.
Let me show you a quick example:
module Reportable
def report
"This car runs on #{fuel}."
end
end
class PetrolCar
include Reportable
def fuel
"petrol"
end
end
class DieselCar
include Reportable
def fuel
"diesel"
end
end
Now, if we try our classes in irb
:
>> pcar = PetrolCar.new
=> #<PetrolCar:0x007fda3403bba8>
>> pcar.report
=> "This car runs on petrol."
>> dcar = DieselCar.new
=> #<DieselCar:0x007fda3318a320>
>> dcar.report
=> "This car runs on diesel."
You see, the implementation of the Reportable#report
method relies on (or, is
coupled to) the implementation of the DieselCar
and PetrolCar
classes. If
we mixed in the Reportable
mixin in a class that does not have the fuel
method implemented, we would get an error when calling the report
method.
Testing coupled mixins #
When it comes to coupled mixins, testing can get just a tad bit harder. Again, the same two strategies.
class ReportableTest < Minitest::Test
def setup
@test_obj = Object.new
@test_obj.extend(Reportable)
class << @test_obj
def fuel
"diesel"
end
end
end
def test_speed_reported
assert_equal "This car runs on diesel.", @test_obj.report
end
end
Now, you can see, it gets a bit hairy when we open the singleton class of the
@test_obj
and we add the fuel
method, so our coupled mixin can work. But,
otherwise, it’s quite straight forward.
The approach that I prefer is using the dummy class because it’s much more explicit.
class DummyCar
include Reportable
def fuel
"gasoline"
end
end
class ReportableTest < Minitest::Test
def test_fuel_reported
dummy = DummyCar.new
assert_equal "This car runs on gasoline.", dummy.report
end
end
Now, this is cleaner. We create a DummyCar
class where we mix the
Reportable
mixin and we define the fuel
method. Then, in the test, we just
create a DummyCar
object and we assert for the value of the report
method.
Remember, we are doing this only because we want to test the mixin. If we were
to test any of the classes, there would be no point in doing this.
As you can see, it’s quite simple to test these mixins with plain-old Ruby. I guess it’s worth mentioning that these examples are very simple and you might run into more complex mixins that need testing in the future. But, when testing mixins, these strategies will still work. Just don’t get owerwhelmed and take it step-by-step.