Introduction
Mixins are a very powerful feature in Ruby, but knowing how to test them is sometimes not too obvious, especially to beginners. This stems from mixins’ nature – they get mixed into other classes. In this tutorial, we will revisit what mixins are, identify mixin types, and learn how we can test mixins with the most popular testing tools for Ruby, Minitest and RSpec.
Mixins
First, let’s remind ourselves what Mixins are. Simply put, modules are a way of grouping together methods, classes, and constants. Modules in Ruby come in two flavours – a module which is used for namespacing or separation, and modules that implement the mixin facility.
Mixins are a mechanism to avoid multiple inheritance. One can include a module within a class definition. When this is the case, all of the module’s instance methods also become available as instance methods in the class. They get mixed in, which is where the term mixin comes from. If you think about it, mixins in fact behave like superclasses.
We usually write two types of mixins in Ruby: coupled and uncoupled mixins.
Enumerable
The Enumerable
module is possibly Ruby’s most popular mixin, so it deserves a honorable mention in this article. It provides collection classes with a lot of traversal, searching and sorting methods. When a class implements the Enumerable
for its traversal behaviour, it must implement an each
method, which returns each of the sequential members in the collection.
Also, when a class implements the Enumerable
mixin for sorting or comparing behaviour, like the Enumerable#max
, #min
or #sort
methods, the objects in the collection must also implement a working <=>
operator. Its purpose is to allow comparison between the objects in the collection.
For more details on the Enumerable mixin, you can read its official documentation.
Uncoupled Mixins
Uncoupled mixins are the ones whose methods do not depend on the implementation of the class where they will get mixed in.
Here’s an example of an uncoupled mixin:
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 in the example, the speed
method in the Speedable
mixin does not depend on any other methods that are defined in classes where it’s mixed in. In other words, the mixin is self-contained, or uncoupled.
Coupled Mixins
Coupled mixins are mixins whose methods depend on the implementation of the class where they are mixed in. They are the exact opposite of uncoupled mixins.
Here’s an example of a coupled mixin:
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."
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 Mixins
The two most popular choices of testing tools for Ruby are RSpec and Minitest. So, let’s see how we can leverage these two testing tools when testing mixins.
Testing Uncoupled Mixins
Testing uncoupled mixins is quite trivial. There are two main strategies you can use — extending the singleton class of an object, or using a dummy class.
Let’s see the first one.
Testing Uncoupled Mixins With Minitest
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, we assert in the test 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, we just create a new object in the test from the dummy class and assert on the dummy.speed
method.
Testing Uncoupled Mixins with RSpec
The only difference between Minitest and RSpec is the syntax. The logic behind the testing strategy is the same.
describe FastCar
before(:each) do
@test_obj = Object.new
@test_obj.extend(Speedable)
end
it "reports the speed" do
expect(@test_obj.speed).to eq "This car runs super fast!"
end
end
As you can see, the strategy is the same. We use a plain Object
and extend its singleton class. Then, we set the expectations in our tests.
class DummyTestClass
include Speedable
end
describe FastCar
let(:dummy) { DummyTestClass.new }
it "reports the speed" do
expect(dummy.speed).to eq "This car runs super fast!"
end
end
Again, the major difference here is the syntax. We introduce a new DummyTestClass
class, where we mix in the Speedable
mixin. Then, using RSpec’s let
syntax, we create a new object of the DummyTestClass
class and set our expectations on it.
Testing Coupled Mixins
When it comes to coupled mixins, testing can get a bit more difficult. Again, the same two strategies apply here.
Testing Coupled Mixins With Minitest
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
As you can see, things get a bit complicated when we open the singleton class of the @test_obj
and add the fuel
method so our coupled mixin can work. However, it’s a quite straightforward approach other than that.
It’s better to use a dummy class because this approach is 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
We create a DummyCar
class, in which we mix the Reportable
mixin and define the fuel
method. Then, we just create a DummyCar
object in the test and 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.
Testing Coupled Mixins with RSpec
Here is the first strategy:
describe Reportable
before(:each) do
@test_obj = Object.new
@test_obj.extend(Reportable)
class << @test_obj
def fuel
"diesel"
end
end
end
it "reports the fuel type" do
expect(@test_obj.report).to eq "This car runs on diesel."
end
end
And the second strategy:
class DummyCar
include Reportable
def fuel
"gasoline"
end
end
describe Reportable
let(:dummy) { DummyCar.new }
it "reports the fuel type" do
expect(dummy.report).to eq "This car runs on gasoline."
end
end
Outro
In this tutorial, we reminded ourselves what Mixins are, how they work in Ruby, and how they improve our classes. We identified two types of Mixins, learned about different mixin testing strategies and covered testing with examples written in the two most popular testing libraries for Ruby, RSpec and Minitest.
Although the examples that we covered are quite small, these strategies are applicable to larger mixins as well. What strategies do you prefer when testing Mixins? What are the drawbacks with the strategies that you use? Feel free to join the discussion in the comments below.
P.S. Would you like to learn how to build sustainable Rails apps and ship more often? We’ve recently published an ebook covering just that — “Rails Testing Handbook”. Learn more and download a free copy.