Saturday, March 24, 2007

Rails: ActiveRecord Unit Testing part II

Back in December I wrote about unit testing ActiveRecord models. Following that post, we started using that pattern to unit test our models. It quickly became obvious that the ActiveRecord::ConnectionAdapters::Column.new("digits", nil, "string", false) statements were largely static and we wrapped this behavior in a fluent interface.

This worked, but some developers on the team didn't like the (arguably) unnecessary additional syntax. These developers argued that since none of our Unit Tests hit the database we should simply override the new method and handle stubbing the columns method.

You can argue that adding magic to the new method could be confusing, but our team has the standard that no unit test can hit the database. Therefore, everyone is very aware that, despite the standard syntax, we are injecting our own column handling mechanism.

The code to make this work is fairly straightforward. The usual alias method gotchas apply so I chose to use an alternative. Adding the following code will allow you to create disconnected models in your unit tests simply by calling new.
class << ActiveRecord::Base

new_method = instance_method :initialize
define_method :initialize do |*attributes|
attributes = attributes.empty? ? nil : attributes.first
self.stubs(:columns).returns(no_db_columns(attributes))
new_method.bind(self).call attributes
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

4 comments:

  1. Jay,

    I'm on a large Rails project (160+ models) and we've been trying to clean up our tests and also make them much faster than they are now. We've been trying some of the techniques you bring up and I'm trying to get my head around where certain things work well (and how they affect how we need to write our tests if we're trying them), and where something else is needed.

    Would it be accurate to say that, to use the "don't touch the database in a unit test" model we're adopting some other rules as well, including:

    * mock everything that is not the object under test
    * mock instance methods that would create or return other real models (Foo.bars.create, Foo.bars, Bar.create!)

    I'm probably missing something, but it seems perhaps that certain things we might write in our models get hard to test without touching the database (I suspect this may be a case of "you're doing that wrong", but it would be good to know that that's the case). I'm thinking specifically of instance methods that alter the state of the object and then save the object. For example we have a ScheduleEntry model (think appointments, but there are some other customer requirements involved) that has a #keep! method which updates the status of the ScheduleEntry to 'kept' (we are using acts_as_state_machine) and then calls a #save! on the object. On the one hand perhaps that's a bad way to do it, but on the other do we need to mock/stub #save! to make things happy when testing away from the database?

    Another question is, is there a way to apply this sort of thinking to functional tests (for us our functional tests take 2-3x as long as our unit tests)? There's one place in our codebase where we are introducing a Presenter, which is making the controller code much clearer and easier to test (and the Presenter is very easy to test), but the Presenter isn't something we want to introduce to well over 100 controllers. Most of the RESTful controllers have an #update and/or #create action, which we typically test with an 'assert_difference Foo, :count {}' block as it's nice to know that a successful create call actually creates the object in question. It may be frivolous to actually check the difference, or it may be satisfactory just to mock the create/create! methods for the model and just check that the mock was called once, not sure. I'm just thinking out loud to some degree to see what issues we might run into down the road if we try to sever our database connection during testing.

    Thanks for the great series of articles.
    Rick

    ReplyDelete
  2. Anonymous12:45 PM

    Hello Rick,

    While I like the 'mock everything that is not the object under test' rule, I don't think it's tied to 'don't touch the database'. You could build a graph of disconnected activerecord objects by stubbing the association methods, but still use the real activerecord objects in your test.

    In general I do mock the save or create methods when I'm writing unit tests. Since they require a db hit, it's going to always be necessary.

    I've really not found any code that I can't unit test without hitting the database, but I would be very interested to see an example that proved otherwise. For your specific example I would, as you suggested, mock the save! method (in a unit test).

    For functional tests, we try to only mock external dependencies (read services).

    As far as the assert_difference method, there does sound like theres value in the tests you are writing. I think that you would want functional tests even if you came up with a way to unit test it, so you won't see an improvement in test running time by creating a unit test. There's still value if you can come up with a unit test since it will fail faster, but it wont help your overall build time. Unfortunately, I haven't needed to do anything with the RESTful stuff so I can't provide any ideas.

    Let me know what you come up with.

    Cheers, Jay

    ReplyDelete
  3. I love the idea of not touching the database during unit testing, but if I restrain myself that way, I don't know how to test associations.

    Enter a complicated association:
    If a Clinic habtm media, a Medium habtm tags, and a Tag belongs_to sport, I think I need finder_sql for the Clinic#sports and Sport#clinics associations. How do I test these associations?

    ReplyDelete
  4. Anonymous9:29 PM

    Hello m@

    I don't know of an easy way to test that without hitting the database. You might be able to find one, but I'd opt for putting a test like that in the functional test suite since there shouldn't be many of them. Not to mention that associations are fairly database centric so should probably be tested at the functional level anyway.

    Cheers, Jay

    ReplyDelete

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