Helper for creating table and model in Rails tests

Here’s a simple method that you can throw into your test_helper.rb (or wherever helpers go in your test framework) that works correctly as of Rails 7.0 and Ruby 3.2. This method creates your table, returns the model class, and also the teardown proc you can call yourself. The name of the table is thread safe, because it uses process id and thread id by default.

def make_test_model \
  suffix: nil,
  name: "test_model_#{suffix}_#{$$}_#{Thread.current.object_id}".squeeze('_'),
  parent: 'ApplicationRecord',
  **table_options,
  &block

  table_options.with_defaults!(force: true)
  ActiveRecord::Base.connection.create_table(name, **table_options, &block)
  model = Class.new(parent.constantize) { self.table_name = name }
  [model, -> { ActiveRecord::Base.connection.drop_table(name) }]
end

Here’s the usage example for Minitest:

require 'test_helper'

class MyTest < ActiveSupport::TestCase
  setup do
    @model, @model_teardown = make_test_model(id: :uuid) { |t|
      t.string :first_name
      t.string :last_name
      t.timestamps
    }
  end

  teardown { @model_teardown.call }

  test "something" do
    model = @model.create(first_name: 'Max')
    assert_predicate model, :persisted?
  end
end

Here’s the usage example for RSpec (untested, but you get the gist):

RSpec.describe Something do
  before do
    @model, @model_teardown = make_test_model(id: :uuid) { |t|
      t.string :first_name
      t.string :last_name
      t.timestamps
    }
  end

  after { @model_teardown.call }

  it "does something" do
    model = @model.create(first_name: 'Max')
    expect(model).to be_persisted
  end
end

Options

  • name: 'table_name' — lets you specify a custom table name, but be mindful that thread safety is now on you.
  • suffix: 'my_suffix' — lets you specify a custom part of table name, while preserving thread safety.
  • parent: 'MyRecord' — lets you specify a class to inherit (ApplicationRecord by default).
  • **table_options — other options are sent straight to create_table, so stuff like id: :uuid will work.
  • &block — pass the migration block, and you get the t arg, on which you can declare the table schema as usual.

Model class name

If you need a real constant name for the model, feel free to add this to your setup:

RealConst = @model

but again, thread-safety is on you.

To use the same unique name as the table, do something like this:

Object.const_set(@model.table_name.classify, @model)

To teardown the constant, use :remove_const:

Object.send :remove_const, @model.table_name.classify

Opinion on existing solutions

I found 2 gems (temping and with_model) for creating temporary models and tables in rails tests. They felt overcomplicated to me, because they tried to take over setup/teardown, or integrate with RSpec too much. Instead, I’d like to be given tools to make my own setup/teardown in any test framework. These gems also have a lot of code to support various Rails and Ruby versions. I think it’s easier to support a small method in-house.


Code snippets in this post are covered by MIT License.



Date
July 9, 2023