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