At Pivotal, we are passionate about test driven development, keeping things DRY, and writing readable and understandable code. Satisfying all of these desires can be challenging, especially when writing test code. In particular, ActiveRecord
extensions present several challenges: which models using an extension should we test? How do we both test our extension in isolation while also testing all model’s usage of that extension? Is it even worth it?
The answer is yes, it is worth it, and it’s also fairly easy, readable, understandable, and DRY. I will present both a common problem and a solution, using a cumulation of technologies and techniques from multiple Pivotal projects, in particular using acts_as_fu to create laser-targeted, isolated, and disposable ActiveRecord
models for testing extensions and RSpec shared behaviors to minimize the amount of duplicated test code.
Often we find common patterns in ActiveRecord
models and we wish to share that functionality by mixing in a module
of shared code, or even mixing that module in to ActiveRecord::Base
itself. How should we go about testing these ActiveRecord
extensions? Not only do we want to test the extension, but also test that models using that extension are doing so properly. We’ve blogged about dynamically creating ActiveRecords to test extensions in the past, but Pivot Pat Nakajima’s acts_as_fu plugin is a far better tool for this.
The Setup
Let’s say we have three models: Pivotal
, Lab
, and Pivot
. Both a Pivot
and a Lab
belongs_to
Pivotal
, have a name
, nickname
, some common validation, etc:
# app/models/pivotal.rb
class Pivotal < ActiveRecord::Base
end
# app/models/lab.rb
class Lab < ActiveRecord::Base
RANDOM_NICKNAMES = ["New Hotness", "LOL-Cat Factory", "Tweet Machine"]
belongs_to :pivotal
validates_presence_of :name
def nickname
"#{self.name}, 'The #{RANDOM_NICKNAMES[rand(RANDOM_NICKNAMES.length)]}'"
end
end
# app/models/pivot.rb
class Pivot < ActiveRecord::Base
RANDOM_NICKNAMES = ["New Hotness", "LOL-Cat Factory", "Tweet Machine"]
belongs_to :pivotal
validates_presence_of :name
def nickname
"#{self.name}, 'The #{RANDOM_NICKNAMES[rand(RANDOM_NICKNAMES.length)]}'"
end
end
The specs for Lab and Pivot might look like the following:
# spec/models/lab_spec.rb
describe Lab do
before(:each) do
@lab = Lab.new
end
it "should require name" do
@lab.should have(1).errors_on(:name)
@lab.name = 'Stealth Startups'
@lab.should have(0).errors_on(:name)
end
it "should generate a random nickname" do
@lab.name = 'Stealth Starup'
@lab.nickname.should_not be_blank
@lab.nickname.should include(@lab.name + ", 'The ")
end
it "should belong to Pivotal" do
@lab.should respond_to(:pivotal)
@lab.should respond_to(:pivotal=)
@lab.should respond_to(:pivotal_id)
@lab.should respond_to(:pivotal_id=)
end
end
# spec/models/pivot_spec.rb
describe Pivot do
before do
@pivot = Pivot.new
end
it "should require name" do
@pivot.should have(1).errors_on(:name)
@pivot.name = 'Joe Moore'
@pivot.should have(0).errors_on(:name)
end
it "should generate a random nickname" do
@pivot.name = 'Joe Moore'
@pivot.nickname.should_not be_blank
@pivot.nickname.should include(@pivot.name + ", 'The ")
end
it "should belong to Pivotal" do
@pivot.should respond_to(:pivotal)
@pivot.should respond_to(:pivotal=)
@pivot.should respond_to(:pivotal_id)
@lab.should respond_to(:pivotal_id)
end
end
DRYing Up the Models
Yuck, look at all that duplication! Let’s start eliminating it by pulling the common model code into an ActiveRecord
extension named belongs_to_pivotal
:
# lib/belongs_to_pivotal.rb
module BelongsToPivotal
RANDOM_NICKNAMES = ["New Hotness", "LOL-Cat Factory", "Tweet Machine"]
module ClassMethods
def belongs_to_pivotal
belongs_to :pivotal
validates_presence_of :name
instance_eval do
include BelongsToPivotalInstanceMethods
end
end
end
module BelongsToPivotalInstanceMethods
def nickname
"#{self.name}, 'The #{BelongsToPivotal::RANDOM_NICKNAMES[rand(BelongsToPivotal::RANDOM_NICKNAMES.length)]}'"
end
end
def self.included(base)
base.extend(ClassMethods)
end
end
Now our Models look like this:
# app/models/lab.rb
class Lab < ActiveRecord::Base
belongs_to_pivotal
end
# app/models/pivot.rb
class Pivot < ActiveRecord::Base
belongs_to_pivotal
end
You’ll need to add “ActiveRecord::Base.send :include, BelongsToPivotal
” to config/initializers/new_rails_defaults.rb
or some other initializer.
Testing the Extension with acts_as_fu
The models are looking better, but what about the specs? In the “old days” I would create a spec named belongs_to_pivotal_spec.rb
and use one of the two Models in that spec. But, when you do that, you get all the the “baggage” from that Model, such as any other methods, associations, inherited methods and properties, etc. Let’s use acts_as_fu
to write a spec that tests BelongsToPivotal
in isolation.
# spec/lib/belongs_to_pivotal_spec.rb
describe BelongsToPivotal do
before(:all) do
# Using acts_as_fu to create a model specifically for our extension
build_model :belongs_to_pivotal_models do
# we will need these columns in the database
string :name
integer :pivotal_id
# Call our extension here
belongs_to_pivotal
end
end
before(:each) do
@pivotal_model = BelongsToPivotalModel.new
end
# Look, it's all of the model specs!
it "should require name" do
@pivotal_model.should have(1).errors_on(:name)
@pivotal_model.name = 'Pivotal Model'
@pivotal_model.should have(0).errors_on(:name)
end
it "should generate a random nickname" do
@pivotal_model.name = 'Pivotal Model'
@pivotal_model.nickname.should_not be_blank
@pivotal_model.nickname.should include(@pivotal_model.name + ", 'The ")
end
it "should belong to Pivotal" do
@pivotal_model.should respond_to(:pivotal)
@pivotal_model.should respond_to(:pivotal=)
@pivotal_model.should respond_to(:pivotal_id)
@pivotal_model.should respond_to(:pivotal_id=)
end
end
Now that our ActiveRecord
extension is well tested, how do we make sure that our two models are actually using it? One technique is to check that each model responds to the specific methods added by our extension:
#spec/models/lab_spec.rb
describe Lab do
...
it "should belong_to_pivotal" do
@lab.should respond_to(:pivotal)
@lab.should respond_to(:pivotal=)
@lab.should respond_to(:pivotal_id)
@lab.should respond_to(:pivotal_id=)
@lab.should respond_to(:name)
@lab.should respond_to(:name=)
@lab.should respond_to(:nickname)
end
...
This does not feel very satisfying. We are duplicating some of the tests from belongs_to_pivotal_spec.rb
and not verifying that we are getting the validations. A crazy coincidence could result in these methods all being defined without actually using our extension.
Another technique, though some would call it a hack, is to provide a hook within the extension itself so we can check for it later:
# lib/belongs_to_pivotal.rb
module BelongsToPivotal
...
module ClassMethods
...
# We can check this to see if a model uses this extension
def belongs_to_pivotal?
self.included_modules.include?(BelongsToPivotalInstanceMethods)
end
end
...
end
Let’s update belongs_to_pivotal_spec.rb
to test this method:
# spec/lib/belongs_to_pivotal_spec.rb
describe BelongsToPivotal do
before(:all) do
# Using acts_as_fu to create a model specifically for our extension
build_model :belongs_to_pivotal_models do
...
end
# Create a model that does not use our extension
build_model :never_belongs_to_pivotal_models do
# do nothing
end
end
...
it "should know if it belongs_to_pivotal" do
BelongsToPivotalModel.belongs_to_pivotal?.should be_true
NeverBelongsToPivotalModel.belongs_to_pivotal?.should be_false
end
end
#spec/models/lab_spec.rb
describe Lab do
it "should belong_to_pivotal" do
Lab.belongs_to_pivotal?.should be_true
end
end
#spec/models/pivot_spec.rb
describe Pivot do
it "should belong to Pivotal" do
Pivot.belongs_to_pivotal?.should be_true
end
end
Using RSpec Shared Behaviors
How much further can we go? Notice that our two Model specs are 5 whole lines long! Unacceptable! All kidding aside, we can DRY this up just a bit more by using RSpec’s shared behaviors.
In spec/spec_helper.rb
# spec/spec_helper.rb
describe 'it belongs to pivotal', :shared => true do
it "should belongs_to_pivotal" do
described_class.belongs_to_pivotal?.should be_true
end
end
Now we can use this shared behavior in our specs:
#spec/models/lab_spec.rb
describe Lab do
it_should_behave_like "it belongs to pivotal"
end
#spec/models/pivot_spec.rb
describe Pivot do
it_should_behave_like "it belongs to pivotal"
end
I hope that these techniques are helpful. Feel free to post your own!
About the Author