It turns out that it’s really easy to create tables and models dynamically within a Rails unit test. It’s a useful technique for reducing dependencies when testing.

To explain why you’d want to do this, imagine a hypothetical acts_as_melon_farmer module that extends some models in your Rails application with additional behaviour:

class Foo < ActiveRecord::Base
  acts_as_melon_farmer
end
class Bar < ActiveRecord::Base
  acts_as_melon_farmer
end

What acting as a melon farmer really means is beyond the scope of this exercise.

The operation of the module depends on the ActiveRecord classes which it extends: in order to test the module, you need to provide that basic functionality.

One way to do this is to use an existing concrete class (Foo or Bar in the example). This works, but the tests are messy, poorly delineated, and brittle.

A better solution, perhaps, is to use mocking and stubbing to handle the interface between the module and ActiveRecord. However, one thing that’s very difficult to mock is the database—especially in ActiveRecord, where the SQL is exposed at a high level. If you want to test the actual behaviour of a particular query, you need to put it through the database layer. That means that you need a model definition and an associated database table.

As this model has no meaning outside the test, wouldn’t it be nice if you could create it in an entirely self-contained manner? Well, you can:

class ActsAsMelonFarmerTest < Test::Unit::TestCase

  class Migration < ActiveRecord::Migration
    def self.up
      create_table 'melon_farmer_test_objects', :force => true do |t|
        # column definitions
      end
    end

    def self.down
      drop_table 'melon_farmer_test_objects'
    end
  end

  class MelonFarmerTestObject < ActiveRecord::Base
    acts_as_melon_farmer
  end

  def setup
    Migration.up
  end

  def teardown
    Migration.down
  end

  # Your tests go here
end

I love it when there’s a clean solution to a problem.

I also need to pass on credit to Chris for suggesting that database migrations might work, and Ben, with whom I worked on this particular piece of code.