Testing ActiveRecord concerns in isolation without including them in an application model can be challenging.

Consider an ActiveRecord concern that extracts a specific database behavior. For example, a query for tagging:

module TaggableConcern
  extend ActiveSupport::Concern

  included do
    scope :with_tag, ->(tag) {
      where('? = ANY(tags)', tag)
    }

    scope :with_tags, ->(tags) {
      where('tags @> ARRAY[?]::varchar[]', Array(tags))
    }
  end
end

To test this cleanly, you can leverage a specific PostgreSQL feature found in the CREATE TABLE synopsis: temporary tables.

CREATE TEMPORARY TABLE table_name (...)
ON COMMIT { PRESERVE ROWS | DELETE ROWS | DROP }

Using this in Rails is straightforward. The [create_table]((https://api.rubyonrails.org/v6.0/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-create_table) method accepts :temporary as a boolean argument to mark the table as ephemeral. It also accepts an options parameter to define extra table instructions, such as when to drop it:

create_table :my_table, temporary: true, options: 'ON COMMIT DROP' do |t|
  ...
end

Here is how to apply this to test the tagging concern in RSpec:

RSpec.describe TaggableConcern, type: :model do
  before do
    class TempBook < ApplicationRecord
      include TaggableConcern

      connection.create_table :temp_books, temporary: true, options: 'ON COMMIT DROP' do |t|
        t.string :name
        t.string :tags, array: true

        t.timestamps
      end
    end
  end

  let!(:ruby) do
    TempBook.create!(
      name: 'Programming Rails',
      tags: %w[ruby rails web]
    )
  end

  let!(:elixir) do
    TempBook.create!(
      name: 'Programming Phoenix',
      tags: %w[elixir phoenix web]
    )
  end

  let!(:go) do
    TempBook.create!(
      name: 'Programming Go',
      tags: %w[go programming]
    )
  end

  describe '.with_tag' do
    subject { TempBook.with_tag(query) }

    context 'with nil value' do
      let(:query) { nil }

      it { is_expected.to be_empty }
    end

    context 'with one match' do
      let(:query) { 'ruby' }

      it { is_expected.to match_array([ruby]) }
    end

    context 'with multiple matches' do
      let(:query) { 'web' }

      it { is_expected.to match_array([ruby, elixir]) }
    end
  end

  describe '.with_tags' do
    subject { TempBook.with_tags(query) }

    context 'with nil value' do
      let(:query) { nil }

      it { is_expected.to be_empty }
    end

    context 'with empty array' do
      let(:query) { [] }

      it { is_expected.to be_empty }
    end

    context 'with a common tag' do
      let(:query) { 'web' }

      it { is_expected.to match_array([ruby, elixir]) }
    end

    context 'with a common tag as array' do
      let(:query) { ['web'] }

      it { is_expected.to match_array([ruby, elixir]) }
    end

    context 'with mixed tags' do
      let(:query) { ['ruby', 'elixir'] }

      it { is_expected.to be_empty }
    end

    context 'with mixed tags' do
      let(:query) { ['ruby', 'web'] }

      it { is_expected.to match_array([ruby]) }
    end
  end
end
Finished in 0.1783 seconds (files took 0.59146 seconds to load)
9 examples, 0 failures