Another solution for disconnected unit tests

Update (9/5/07): My project has
since switched over to UnitRecord
for the reasons outlined by Dan
Manges
.

Along with many others, I believe that unit tests should have as few
dependencies as possible and be very fast. In rails, this means
disconnecting models from the database when running unit tests.

My project uses the following solution, which is derived from the
solution posted at Jay’s blog: Rails: ActiveRecord Unit Testing part
II

The difference is that this solution does not unbind and rebind the
initialize method. Instead, it uses the inherited hook to include a
module with the necessary code on each ActiveRecord::Base subclass. My
team and I like this solution better since it doesn’t unbind and
redefine methods, but rather uses ruby’s object model to insert code in
the call hierarchy between the model and ActiveRecord::Base.

The full solution belongs in a file (unit_test_helper.rb) that is
required by each unit test:

module DisconnectedModel  
  def initialize(attributes={})
    self.class.stubs(:columns).returns(no_db_columns(attributes))
    super
  end

  def no_db_columns attributes
    return [] if attributes.nil?
    attributes.keys.collect do |attribute|
      sql_type = case attributes[attribute].class
      when " " then "integer"
      when "Float" then "float"
      when "Time" then "time"
      when "Date" then "date"
      when "String" then "string"
      when "Object" then "boolean"
      end
      ActiveRecord::ConnectionAdapters::Column.new(attribute.to_s, nil, sql_type, false)
    end
  end
end

class ActiveRecord::Base  
  def self.inherited(cls)
    cls.class_eval do
      include DisconnectedModel
    end
  end
end

class << ActiveRecord::Base  
  def connection
    raise 'You cannot access the database from a unit test'
  end
end  

Models can be tested by passing in the columns and their values into the
new method:

require File.dirname(__FILE__) + '/../unit_test_helper'

class DogTest < Test::Unit::TestCase  
  def test_name
    dog = Dog.new(:name => "fido")
    assert_equal "fido", dog.name
  end
end  

Models can even have their own initialize method as long as it calls
super:

def test_translate_age_into_dog_years  
  dog = Dog.new(:age => 3)
  assert_equal 21, dog.age
end

class Dog < ActiveRecord::Base  
  def initialize(args={})
    human_age = args[:age] || 0
    super(args.merge(:age => human_age * 7))
  end
end