ActiveRecord callback Tests mit RSpec

ActiveRecord callbacks sind Trigger, die beim Persistieren ausgelöst werden und sich innerhalb einer Transaktion befinden.
Sobald sie exzessiv eingesetzt werden oder sogar externe Prozesse auslösen (z. Bsp. andere Objekte anlegen/ ändern/ löschen oder externe Systeme ansprechen) können sie problematisch werden. Wenn das System wächst, entstehen dann schnell Trigger Ketten, die schwer zu beherrschen sind.
Wenn allerdings ein bestimmter interner Zustand abgesichert werden soll, können sie durchaus sinnvoll sein.
Natürlich müssen diese callbacks getestet werden. Im Folgenden wird dafür RSpec genutzt.
Als Beispiel sollen Produkte dienen. Das Modell:

rail$ rails g model Product name:string warranty_months:integer && rake db:migrate

Die leere Klasse:

class Product < ActiveRecord::Base                                              
end

Jedes Produkt hat einen Namen und eine Garantiedauer in Monaten. Es soll bei der Erstellung eine Standardgarantie von 6 Monaten erhalten, wenn keine definiert wurde.
Erste naive Tests, die die Anforderung beschreiben:

require 'spec_helper'

describe Product do
  subject { Product.new name: 'Computer' }

  context "when created" do
    it "should have 6 months warranty by default" do
      subject.warranty_months = nil
      subject.save
      expect(subject.warranty_months).to be(6)
    end

    it "should keep its defined months warranty" do
      subject.warranty_months = 12
      subject.save
      expect(subject.warranty_months).to be(12)
    end
  end
end

Die Implementierung der angeforderten Funktionalität:

class Product < ActiveRecord::Base                                              
  before_create :set_warranty_months                                            
                                                                                
  private                                                                       
  def set_warranty_months                                                       
    self.warranty_months ||= 6                                                  
  end                                                                           
end

Danach geben die Tests grünes Licht:

Finished in 0.09989 seconds (files took 1.87 seconds to load)
2 examples, 0 failures

Allerdings werden für die Tests zwei Produkte persistiert. Das ist unnötig. Es soll nicht getestet werden, ob Ruby on Rails korrekt persistieren kann. Es soll lediglich getestet werden, ob das Produkt eine definierte Garantiedauer hat, wenn es gespeichert wird. Da das über callbacks sicher gestellt werden soll, müssen nur die callbacks selber gestestet werden:

require 'spec_helper'

describe Product do
  subject { Product.new name: 'PC' }

  context "when created" do
    it "should have 6 months warranty by default" do
      subject.warranty_months = nil
      subject.run_callbacks :create
      expect(subject.warranty_months).to be(6)
    end

    it "should keep its defined months warranty" do
      subject.warranty_months = 12
      subject.run_callbacks :create
      expect(subject.warranty_months).to be(12)
    end
  end
end

Inhaltlich hat sich an den Tests nichts geändert. Es wird jeweils lediglich statt save, run_callbacks aufgerufen. Allerdings wird in den Tests nun deutlich, dass das Anlegen von Objekten getestet wird und sie benötigen weniger Zeit:

Finished in 0.02026 seconds (files took 1.87 seconds to load)
2 examples, 0 failures

Es lohnt sich der Vergleich:

  1. vorher: 0.09989 seconds
  2. nachher: 0.02856 seconds