Close
Glad You're Ready. Let's Get Started!

Let us know how we can contact you.

Thank you!

We'll respond shortly.

LABS
DRY, Targeted, and Reusable Testing of ActiveRecord Extensions

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!

Comments
Post a Comment

Your Information (Name required. Email address will not be displayed with comment.)

* Copy This Password *

* Type Or Paste Password Here *