Unit testing (Rails) ActiveRecord classes without a database
On Christmas Jay blogged about unit testing ActiveRecord classes:
class PhoneNumberTest < Test::Unit::TestCase
Column = ActiveRecord::ConnectionAdapters::Column
test "to_formatted_s returns US format" do
PhoneNumber.stubs(:columns).returns([Column.new("digits", nil, "string", false)])
number = PhoneNumber.new(:digits => "1234567890")
assert_equal "(123) 456-7890", number.to_formatted_s
end
end
We don't hit the database in unit tests. Having disabled database access on our unit tests, I started using the above approach to write some unit tests for some active record classes that did not have suitable unit tests. On writing a couple of tests though, it was clear this would be too cumbersome:
class PersonTest < Test::Unit::TestCase
Column = ActiveRecord::ConnectionAdapters::Column
test "full name concatenates" do
Person.stubs(:columns)
.returns(
[Column.new("first_name", nil, "string", false),
Column.new("middle_initial", nil, "string", false),
Column.new("last_name", nil, "string", false)])
person = Person.new(:first_name => "Ross",
:middle_initial=>"J",
:last_name=>"Pettit")
assert_equal "Ross J Pettit", person.full_name
end
end
Jay suggested a fluent interface:
class PersonTest < Test::Unit::TestCase
test "full name concatenates" do
person = disconnected(Person).where(:first_name => "Ross",
:middle_initial=>"J",
:last_name=>"Pettit")
assert_equal "Ross J Pettit", person.full_name
end
end
Finally, here's how we implemented it:
class Test::Unit::TestCase
class ActiveRecordUnitTestHelper
attr_accessor :klass
def initialize klass
self.klass = klass
self
end
def where attributes
klass.stubs(:columns).returns(columns(attributes))
instance = klass.new(attributes)
instance.id = attributes[:id] if attributes[:id] #the id attributes works differently on active record classes
instance
end
protected
def columns attributes
attributes.keys.collect{|attribute| column attribute.to_s, attributes[attribute]}
end
def column column_name, value
ActiveRecord::ConnectionAdapters::Column.new(
column_name, nil,
ActiveRecordUnitTestHelper.active_record_type(value.class),
false)
end
def self.active_record_type klass
return case klass.name
when "Fixnum" then "integer"
when "Float" then "float"
when "Time" then "time"
when "Date" then "date"
when "String" then "string"
when "Object" then "boolean"
end
end
end
def disconnected klass
ActiveRecordUnitTestHelper.new(klass)
end
end

3 comments:
I saw Jay's approach and was thinking the same thing. One of the larger maintainance projects I'm a part of would become too unwieldy immediately with the one for one approach.
Thanks for this snippet :) It will quickly find a comfortable spot in my rspec and test/unit code.
Good to see you posting on the ruby front as well :)
Looking at the Column.new(...) style, I note that this is just repeating information which already exists, in the form of migrations. In fact, Rails conveniently leaves a db/schema.rb lying around which reflects the most recent schema of the database.
So this suggests an alternative approach for handling disconnected AR objects: read in db/schema.rb into a global data structure, and then override ActiveRecord::Base#columns so that it returns the column info from this.
Then, say, Person.new(:first_name=>'Ross') would be usable as a mock object without any additional work, automatically gaining the relevant accessors.
Brian,
Your comment couldn't be more timely - Dan Manges implemented this (as a plugin for now) and posted it today. See his blog post and .
Post a Comment