Rails: Use fixture helpers like `users(:alice)` in the Rails console

In tests you can write users(:alice) to look up a fixture record. The same helper is not available in the Rails console, so debugging a fixture by name means looking it up by primary key (or by some unique attribute that you have to remember). A small initializer brings the helpers into the console.

Prerequisite: the fixtures must be loaded

This initializer does not load fixture data. It only looks records up by their fixture-derived ID. You have to seed the database first with

bin/rails db:fixtures:load

In the test environment the records are loaded automatically by the test runner.

The initializer

# config/initializers/fixture_console_helpers.rb
Rails.application.configure do
  console do
    require 'active_record/fixtures'

    fixture_root = Rails.root.join('test/fixtures')

    helpers = Module.new do
      Dir.glob(fixture_root.join('**/*.yml')).each do |path|
        fixture_name = Pathname(path).relative_path_from(fixture_root).sub_ext('').to_s
        method_name  = fixture_name.tr('/', '_')

        model_class =
          begin
            fixture_name.classify.constantize
          rescue NameError
            next
          end

        define_method(method_name) do |*labels|
          records = labels.map do |label|
            id = ActiveRecord::FixtureSet.identify(
              label,
              model_class.type_for_attribute(model_class.primary_key).type
            )
            model_class.unscoped.find(id)
          end
          labels.one? ? records.first : records
        end
      end
    end

    TOPLEVEL_BINDING.receiver.extend(helpers)
  end
end

In the console:

$ bin/rails console
>> users(:alice)
=> #<User id: 825285963, name: "Alice", ...>

>> users(:alice, :bob)
=> [#<User id: 825285963, ...>, #<User id: 117902083, ...>]

What the code does

When a Rails console starts, the console block fires once and walks every YAML file under test/fixtures/. For each file it derives a helper name from the path (users.yml becomes users) and maps it back to a model class via classify.constantize . It will also silently skip any files that cannot be resolved.

For each fixture file it then defines a method that takes one or more labels, computes the deterministic record ID with ActiveRecord::FixtureSet.identify (the same call Rails uses during db:fixtures:load), and looks the record up with unscoped.find so default scopes such as multi-tenant filters do not hide it.

The freshly populated module is mixed into the IRB top-level main object via TOPLEVEL_BINDING. It is the same hook Rails uses to expose app, helper, and controller.

Caveats

  • Namespaced fixtures need a matching model. A file at billing/order.yml resolves to Billing::Order. If your fixture directory layout doesn't match the model namespace, classify.constantize raises and the helper is silently skipped.
  • If a top-level method with the same name as a fixture helper already exists in your console environment, extend overrides it.
Felix Eschey