ActiveRecord callbacks are trigger, which are fired during persistence within the transaction.
Once they are used excessively or even trigger external processes (e.g. create/ update/ delete other objects or talk to external systems), they start to bother. Several triggger chain together. Those trigger chains are hard to control.
On the other hand they totally make sense defining internal states.
Nonetheless callbacks have to be tested. Hereinafter RSpec is the testing framework of choice.
The example is about products. The model:
rail$ rails g model Product name:string warranty_months:integer && rake db:migrate
The blank class:
class Product < ActiveRecord::Base
end
Each product has a name and a warranty time in months. It should have set a standard warranty of 6 months after creation, if none defined.
First naive tests describing the requirement:
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
The implementation of the required functionality:
class Product < ActiveRecord::Base
before_create :set_warranty_months
private
def set_warranty_months
self.warranty_months ||= 6
end
end
The tests are returning green lights:
Finished in 0.09989 seconds (files took 1.87 seconds to load)
2 examples, 0 failures
However two products have to be persisted for testing reasons. That is unnecessary. It is useless to test Ruby on Rails persisting correctly. It rather should be tested, if the product has a defined guarantee period, after is was saved. Since that has to be assured with callbacks, the callbacks themselves merely have to be tested:
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
The tests are not changed in their meaning. There is just run_callbacks instead of save. Indeed they are more expressive and take less time:
Finished in 0.02026 seconds (files took 1.87 seconds to load)
2 examples, 0 failures
A comparison pays off:
- before: 0.09989 seconds
- after: 0.02856 seconds