Today I Learned

Making factory_bot work with read-only models

In one of our apps we need to ensure that all models are in read-only mode. Still, for testing purposes we need to be able to create instances of such models. Following code makes it possible.

# spec/models/application_record_spec.rb
require 'rails_helper'

RSpec.describe ApplicationRecord do
  it 'ensures all descendants are read-only' do
    Unit = Class.new(ApplicationRecord)

    expect(Unit.new.readonly?).to eq true
    expect { Unit.create! }.to raise_exception(ActiveRecord::ReadOnlyRecord)
  end

  it 'allows creating objects using factories' do
    Unit = Class.new(ApplicationRecord)

    expect { read_only(:unit) }.to change { Unit.count }.by(1)
  end
  
  it 'disallows updating objects' do
    Unit = Class.new(ApplicationRecord)
    unit = read_only(:unit)

    expect { unit.update_attributes!(name: 'New name') }.to \
      raise_exception(ActiveRecord::ReadOnlyRecord)
  end
end

# spec/support/factory_bot.rb
module FactoryBot
  module Strategy
    class ReadOnly <  Create
      def result(evaluation)
        is_readonly = evaluation.object.readonly?
        evaluation.object.define_singleton_method(:readonly?, -> { false })

        super.tap do |object|
          object.define_singleton_method(:readonly?, -> { is_readonly })
        end
      end
    end
  end
end

FactoryBot.register_strategy(:read_only, FactoryBot::Strategy::ReadOnly)

RSpec.configure do |config|
  config.include FactoryBot::Syntax::Methods
end

# spec/factories/units_factory.rb
FactoryBot.define do
  factory :unit do
    sequence(:name) { |i| "Unit #{i}" }
  end
end