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

4 comments:

  1. 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?

    ReplyDelete
  2. David, 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.

    Thanks 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.

    ReplyDelete
  3. 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.

    I 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.

    ReplyDelete
  4. 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.

    Which 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.

    ReplyDelete

Note: Only a member of this blog may post a comment.