Testing with foreign key constraints

I’m not yet so enlightened that all of my Rails unit and functional tests run without accessing the database. Indeed, I’m still using YAML fixtures to populate the database for testing.

I also insist on having foreign key constraints in the database, a thing that’s not exactly encouraged by Rails, but which is quite possible nonetheless. The various plugins from RedHill Consulting are a big help.

But then, when you feel all warm and cosy due to the additional safety at the database-level, you’re suddenly trapped by a snag: Sooner or later you find that your fixtures contain dependencies among objects that preclude any attempt at clever ordering by violating one foreign key constraint or another. Fixture files are loaded one after another in their entirety and when an object in an earlier fixture refers to an object in a later fixture, the database aptly notices as an inconsistency.

Well, you may think, it is an inconsistency, but only a temporal one. After all the fixture files are loaded, everything is consistent again. That’s the clue. We need to tell the database that, yes, indeed, things may be inconsistent for a time, but we’ll be cleaning up, promise. The good thing is that there is even an SQL standard-compliant way to express this promise.

  START TRANSACTION
  SET CONSTRAINTS ALL DEFERRED
  COMMIT

If you use transactional fixtures, the transaction bracket is already provided by Rails, but there’s no pretty way to sneak in the "SET CONSTRAINTS ..." line. There are two ways of slightly different brutality. First, you can edit activerecord/lib/fixtures.rb and just insert the required line.

  def self.create_fixtures(fixtures_directory, table_names, class_names = {})
    ...
    connection.transaction(Thread.current['open_transactions'].to_i == 0) do
      # insert the following line
      connection.execute("SET CONSTRAINTS ALL DEFERRED")
      ...
    end
    ...
  end

Alternatively, you can overwrite the entire method in, say, <railsapp>/lib/transactional_fixture_loading_hack.rb like this

require 'active_record/fixtures'

Fixtures.class_eval do
  def self.create_fixtures(fixtures_directory, table_names, class_names = {})
    ...
    connection.transaction(Thread.current['open_transactions'].to_i == 0) do
      # inserted line
      connection.execute("SET CONSTRAINTS ALL DEFERRED")
      ...
    end
    ...
  end

Whatever you do, you’ll have to inspect your code whenever you update your Rails version.

We’re still not done, unfortunately. The database defers only those constraints that are deferrable. Have a look at the Foreign Key Migrations Plugin for how to achieve this.

As a matter of convenience, I suggest that in your test_helper.rb you add a method that loads all your fixtures

class Test::Unit::TestCase
  self.use_transactional_fixtures = true

  def self.load_all_fixtures
    fixtures :users, :thingamajigs, :gadgets, :widgets
  end
end

Then, in a testcase class you can use it like this

require File.dirname(__FILE__) + '/../test_helper'

class ThingamajigTest < Test::Unit::TestCase
  load_all_fixtures

  ...
end

Note that with transactional fixtures this results in each fixture file loaded only once for all the tests.

So, there we are a last. Or those with a reasonable DBMS, I might say. For, of course, this technique is no use, if your database does not support deferrable constraints. PostgreSQL for one does support them.

Leave a Reply

Your email address will not be published. Required fields are marked *