Testing modules with RSpec

RSpec supports isolated tests. That makes sense, whenever tests are repeated in different contexts. Modules are classic examples for sure.
Testing shared logic will be described by means of the PORO classes Person and Product:

class Person
end

class Product
end

A premature business logic specification for Person in the person_spec.rb:

require 'spec_helper'

describe Person do                                                                 
  subject { Person.new }                                                           
                                                                                   
  describe "attribute accessors" do
    it "should have name"
  end
                                                                                
  describe "#to_s" do                                                           
    it "should return name"                                                      
                                                                                
    context "when name is undefined" do                                         
      it "should be blank"                                                      
    end                                                                         
  end                                                                 
end

Briefly the Person should receive an attr_accessor :name and a name aliasing method to_s additionally.
Sure, the specifications could be completed and their implementations could be put into Person. The tests would give green lights.
But in the case of having the very same requirements applying to other classes (e.g. Product) likewise, it does not make sense to repeat writing the same specs over and over again. In favour of reusing the specs multiple times, they have to be shareable. The completed tests now are in spec/lib/humanizable_spec.rb:

require 'spec_helper'

shared_examples_for 'Humanizable' do
  subject { described_class.new }

  describe "attribute accessors" do
    it "should have name" do
      subject.name = 'Alice'
      expect(subject.name).to eq('Alice')
    end
  end
  
  describe '#to_s' do
    it "should be equal to name" do                                                
      subject.name = 'Alice'                                                       
      expect(subject.to_s).to eq('Alice')                                          
    end
                                                                            
    context "when name is undefined" do                                            
      it "should be blank" do                                                      
        subject.name = nil                                                         
        expect(subject.to_s).to eq('')                                             
      end                                                                          
    end        
  end
end

Basically there are 2 interesting things going on:

  1. the tests are released for reuse by shared_examples_for (‘Humanizable’)
  2. the context for testing (e.g. Person or Product) is defined by described_class

Everything else is merely routine.
The tests just have to be shared. The former person_spec.rb once again, but shorter:

require 'spec_helper'
require 'lib/humanizable_spec'

RSpec.describe Person do
  it_behaves_like 'Humanizable'
end

There is a declaration, that the class under test has to behave like a ‘Humanizable’. Since the same behaviour is intended to apply to the product as well, the product_spec.rb also can benefit from the shared specs:

require 'spec_helper'
require 'lib/humanizable_spec'

RSpec.describe Product do
  it_behaves_like 'Humanizable'
end

Running the test suite:

$ rspec spec/person.rb

results in failures, as expected, because the implementation is still missing. Therefore the module lib/humanizable.rb has to be like:

module Humanizable
  attr_accessor :name

  def to_s
    name.to_s
  end
end

and finally including it in the classes:

class Person < ActiveRecord::Base
  include Humanizable
end

class Product < ActiveRecord::Base
  include Humanizable
end

makes running the test suite:

$ rspec

return the green lights:

Finished in 0.00394 seconds (files took 1.43 seconds to load)
6 examples, 0 failures