Monday, December 25, 2006

Rails: ActiveRecord Unit Testing

There's been a fair amount of information posted recently on the topic of unit testing ActiveRecord::Base subclasses. Of all the information, I think the most valuable was James' observation that a better data model would result in better tests with less dependencies.

That said, sometimes it would be nice to truly unit test an ActiveRecord::Base subclass. When I say unit test, I mean no dependency on the database. For example, I may store a phone number in the database as a 10 digit string, but I may want to expose that phone number with formatting. I may also want to expose the area code, exchange, and station as methods of the PhoneNumber class. To test this behavior I wrote the following tests that do hit the database.
class PhoneNumberTest < Test::Unit::TestCase

test "to_formatted_s returns US format" do
number = PhoneNumber.new(:digits => "1234567890")
assert_equal "(123) 456-7890", number.to_formatted_s
end

test "area code returns first 3 numbers" do
number = PhoneNumber.new(:digits => "1234567890")
assert_equal "123", number.area_code
end

end
If this syntax looks bizarre at all, you might want to read about how these tests take advantage of the test class method.

The PhoneNumber class has the following implementation.
class PhoneNumber < ActiveRecord::Base

def to_formatted_s
'(' + area_code + ') ' + exchange + '-' + station
end

def area_code
digits[0..2]
end

def exchange
digits[3..5]
end

def station
digits[6..9]
end

end
As the implementation shows, the digits are stored in the database. Splitting the digits up or formatting them is handled by methods on the model.

Now that we have a few tests we'll add the code that disallows database access from unit tests and rerun the tests. As expected the tests fail with the following error.
1) Error:
test_area_code_returns_first_3_numbers(PhoneNumberTest):
ArgumentError: You cannot access the database from a unit test
Looking at the stack trace provided by the error you can track the database access to the columns method of ActiveRecord::Base.

If you want to unit test a model the columns method is a good one to stub. I tried a few different options and the best one I found was stubbing the columns method with a method that returns an array of ActiveRecord::ConnectionAdapters::Column instances. Since I only needed the digits attribute for these tests, it was the only column I needed to return in the array. The following tests test my PhoneNumber class without requiring a trip to the database.
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

test "area code returns first 3 numbers" do
PhoneNumber.stubs(:columns).returns([Column.new("digits", nil, "string", false)])
number = PhoneNumber.new(:digits => "1234567890")
assert_equal "123", number.area_code
end

end
Post a Comment