Tuesday, October 7, 2008

Testing for database insert with ActiveRecord

Often in my tests, I want to test to see whether a database insert occurred; more often I want to make sure that an insert did not happen. I've found a few approaches using mocking, neither of which I was very satisfied with.

Mocking AR's connection's insert


This approach won't win you any code cleanliness awards, but when you absolutely must tell whether or not your code is trying to insert records, this will get the job done.

def test_that_fails_if_any_inserts_happen
flexmock( ActiveRecord::Base.connection ).should_receive( :insert ).never

# code to test
end


This doesn't make an assertion, but flexmock is setting an expectation that the method insert will never be called on AR's connection object before flexmock's teardown gets called.

Mocking model's create


This is a little higher level, but even more deeply coupled with your code.

def test_that_some_model_never_creates
flexmock( MyModel ).should_receive( :create, :create! ).never

# code to test
end

Doing the test this way seems a little cleaner at first, but there's one major caveat: create and create! are only two avenues to doing an insert in the database; this test would pass if the code was refactored to make a raw sql insert, if it used ar-extension's import method, etc. You could remember to change the test if either of those things were to ever happen, but it's best to just avoid this unless you have few options.

How does ActiveRecord test this?


After being unsatisfied with the last two ways to test this, I got the bright idea to look at AR's tests and see how they test this. Surely they need to test whether objects get created, right? Right. This is taken from AR 2.1.1's finder_test.rb, lines 598-604:

def test_find_or_create_from_one_attribute
number_of_companies = Company.count
sig38 = Company.find_or_create_by_name("38signals")
assert_equal number_of_companies + 1, Company.count
assert_equal sig38, Company.find_or_create_by_name("38signals")
assert !sig38.new_record?
end

As you can see, to test whether a database record is created, they take the count of the table before and after the code they're testing, and make sure the final count matches their expectation. Simple.
I was a little underwhelmed by their solution. It's clean, but it's lacking something that I can't put my finger on. Overall. I like their way of testing this over my two solutions, so I've started refactoring my tests to work this way.

Has anyone out there run into this problem, and come up with a better solution?