Module Tests mit RSpec

RSpec unterstützt isoliertes Testen. Das ist sinnvoll, wenn Tests sich in verschiedenen Kontexten wiederholen. Modules sind ein klassisches Beispiel dafür.
Anhand der beiden PORO Klassen Person und Product:

class Person
end

class Product
end

soll geteilte Logik getestet werden.
Eine vorschnelle Definition der Spezifikation der Business Logik für Person in der 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

Kurz gesagt, Person soll einen attr_accessor :name erhalten und zusätzlich soll es eine Aliasmethode to_s auf name geben.
Nun könnten die Spezifikationen ausgefüllt und deren Implemenation in Person eingefügt werden, damit die Tests grünes Licht geben.
Die gleichen Anforderungen gelten allerdings auch für Product. Es macht keinen Sinn, die gleichen Specs noch einmal für Product zu schreiben. Um die Specs mehrfach wiederzuverwenden, müssen sie ausgelagert werden. Die ausgfüllten Tests liegen in nun 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

Interessant sind eigentlich nur 2 Punkte:

  1. die Tests werden mit shared_examples_for (‘Humanizable’) zur Wiederverwendung freigegeben
  2. described_class liefert den Kontext, in dem getestet wird (z.B. Person oder Product)

Der Rest is Formsache.
Jetzt müssen die Tests nur noch genutzt werden. Die ursprüngliche person_spec.rb ist nun natürlich sehr viel kürzer:

require 'spec_helper'
require 'lib/humanizable_spec'

RSpec.describe Person do
  it_behaves_like 'Humanizable'
end

Es wird lediglich deklariert, dass die Klasse sich verhalten soll, wie ‘Humanizable’. Das Gleiche soll ja auch für das Produkt gelten. Die product_spec.rb profitiert ebenfalls von geteilten Specs:

require 'spec_helper'
require 'lib/humanizable_spec'

RSpec.describe Product do
  it_behaves_like 'Humanizable'
end

Nach Durchlaufen der Testsuite:

$ rspec spec/person.rb

werden wie erwartet Fehler gemeldet, da die Implementierung noch fehlt. Sie wird in dem Ḿodul lib/humanizable.rb umgesetzt:

module Humanizable
  attr_accessor :name

  def to_s
    name.to_s
  end
end

und schliesslich verwendet:

class Person < ActiveRecord::Base
  include Humanizable
end

class Product < ActiveRecord::Base
  include Humanizable
end

Die Testsuite:

$ rspec

gibt grünes Licht:

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