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::TestCaseIf this syntax looks bizarre at all, you might want to read about how these tests take advantage of the
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
test
class method.The PhoneNumber class has the following implementation.
class PhoneNumber < ActiveRecord::BaseAs the implementation shows, the
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
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:Looking at the stack trace provided by the error you can track the database access to the columns method of ActiveRecord::Base.
test_area_code_returns_first_3_numbers(PhoneNumberTest):
ArgumentError: You cannot access the database from a unit test
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
I have mixed feelings about this. I REALLY like the idea of eliminating the dependency on the database, but stubbing out AR methods always feels a bit dirty to me because binds my test code to some dark details of AR. Does this bother you at all?
ReplyDeleteDavid, I feel basically the same way, except I lean towards the side of binding my test code to details of AR if it means I don't have to hit the db. I'd prefer a cleaner approach, but it's the lesser of two evils in my mind.
ReplyDeleteThanks for the comment, you've brought up an important detail, this trick only works until they make a breaking change in Rails, at which time a new trick will need to be devised.
Jay, thank you for the helpful article. I would have tried to mock out the whole connection class but mocking out ActiveRecord::Base#columns etc. of course is enough.
ReplyDeleteI would also appreciate Rails allowing real unit tests. In my opinion, the test design is not very clean. Instead of oversimplifying things as they do now, they should allow for any combination of one of {unit test, integration test, system test} and {test for models, test for controllers}. Real tests for views would also be a nice thing to have.
I'm curious as to why you'd want to store a phone number in the database in this fashion. Surely a better approach is to treat the phone number as a value object and encode it using 'composed_of'. That way you get to test your phone number class completely independently of the database.
ReplyDeleteWhich doesn't detract from your main point that stubbing columns is usually the way to go if you want to write unit tests that don't go to the database, of course.
I'm becoming more and more convinced that there's a case for making the model class the authority on its (current) schema. That way, when you're in a testing environment (say) you don't have to stub columns at all. It also has the advantage that information about the models attributes isn't quite so scattered to the four winds.
There's just the simple matter of implementing that idea.