Tuesday, February 21, 2012

 

How to test code in your ActiveRecord after_commit callbacks disabling transactional fixtures per-test without hiding bugs

Okay, so at Goodreads we're finally moving to rails 3.2, and in the process we've discovered the cool after_commit and after_rollback callbacks that easily let you execute things outside the transaction that wraps an active record save/destroy. This was exactly what we needed for a few cases where we were doing non-mission-critical updates in callbacks that can on occasion take a bit of time (hitting memcached or redis servers when our resque queue was overwhelmed) inside transactions.

So we had reason to try converting some of our after_(save|update|destroy|create) callbacks to after_commit calls. The easy step is figuring out how to deal with code that used to do name_changed? or name_was helpers (they retain their expected behavior only within the transaction; once the commit takes place they all get reset). So all we did was keep some after_xxx methods around that set instance variables, then let the after_commit methods read those instance variables (then reset to some disabled state lest we re-process the same code again if models get saved repeatedly for some reason).

Okay, but what about testing? By default, "transactional fixtures" are enabled. I'm not really sure why that's the terminology, as we use this behavior, but I really, really hate testing with fixtures. In fact, I just spent about 18 hours straight yesterday ripping a ton of them out of our codebase to get this all working. But I digress.

The problem is that if we want our after_commit callbacks to fire, they can't be wrapped by transactions around the entire test, because the commit never happens (the transaction gets rolled back after the test runs, pass or fail). So we had to find another way. The only options we could come up with were:

As with any good blog post, we chose door number 3. Reasoning:

So we're using rails 3.2.1, hopefully this is stable across several revisions, I'd hate to have to chase this down again. :( But here's the key:

# activerecord-3.2.1/lib/active_record/fixtures.rb:
...
module ActiveRecord
module TestFixtures
...
def run_in_transaction?
use_transactional_fixtures &&
!self.class.uses_transaction?(method_name)
end
...
def teardown_fixtures
return unless defined?(ActiveRecord) && !ActiveRecord::Base.configurations.blank?

unless run_in_transaction?
ActiveRecord::Fixtures.reset_cache
end

# Rollback changes if a transaction is active.
if run_in_transaction?
@fixture_connections.each do |connection|
if connection.open_transactions != 0
connection.rollback_db_transaction
connection.decrement_open_transactions
end
end
@fixture_connections.clear
end
ActiveRecord::Base.clear_active_connections!
end
...
end
end

So two things:
And here's the code I wrote. I created a module that we just monkey-patch/mix in to ActiveSupport::TestCase. I aimed for a one-liner to deactivate fixtures (and not require an explicit cleanup call at the end, cause someone's gonna forget).

gist here

A few notes:
You just need to call a single method at the top of your test (or a setup method if you want to apply it to a all tests in a suite--again, here's a place I prefer rspec, as you can effectively have a separate setup method for an arbitrary grouping of tests within a given test suite, but meh):

test "some_method is supposed to do something interesting" do
disable_transactional_fixtures # optionally pass in args telling specific tables/models to delete
# do your tests...
# everything gets deleted on magically on its own! :)
end


---

Goodreads is hiring! Please check us out and make the world a better place for readers!

Labels: , , ,


This page is powered by Blogger. Isn't yours?