Testing mixins in isolation with minitest and rspec

Testing Mixins in Isolation with Minitest and RSpec

Learn how to make sure your mixins work properly by testing them in isolation with Minitest and RSpec.

Brought to you by

Semaphore

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. Semaphore is working on a book "The Ultimate Guide to BDD with Rails". Sign up to receive a FREE copy.

81b70354d05caf6bdf7e7953713c1cd4
Ilija Eftimov

Full-stack Ruby on Rails developer. Blogs regularly at eftimov.net.

on this tutorial so far.
User deleted author {{comment.createdAt}}

Edited on {{comment.updatedAt}}

Cancel

Sign In You must be logged in to comment.