Skip to content

Instantly share code, notes, and snippets.

@AlexB52
Last active April 19, 2024 11:44
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save AlexB52/0e186b6bd5220d42351f5cffe47439e7 to your computer and use it in GitHub Desktop.
Save AlexB52/0e186b6bd5220d42351f5cffe47439e7 to your computer and use it in GitHub Desktop.
Testing ActiveRecord Concerns

Testing ActiveRecord Concerns

This gist illustrates the full spec suite of the example used in Testing ActiveRecord Concerns

Copy the testing-concerns.rb file and run rspec testing-concerns.rb

The output should look something like

... bundling ...
... some database logs ...

Finished in 0.04126 seconds (files took 6.61 seconds to load)
10 examples, 0 failures

This gist is available as open-source under the terms of the MIT License.

# frozen_string_literal: true
# -- RAILS SETUP (ignore) --
require "bundler/inline"
gemfile(true) do
source "https://rubygems.org"
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
gem "rails", github: "rails/rails", branch: "main"
gem 'rspec-rails', '~> 4.0.2'
gem "sqlite3"
gem 'byebug'
end
require 'byebug'
require "active_record"
require "minitest/autorun"
require "logger"
require 'rails/all'
# This connection will do for database-independent bug reports.
ActiveRecord::Base.configurations = { "test" => { adapter: "sqlite3", database: "database.sqlite" } }
ActiveRecord::Base.establish_connection ActiveRecord::Base.configurations['test']
ActiveRecord::Base.logger = Logger.new(STDOUT)
# -- RAILS APP --
ActiveRecord::Schema.define do
create_table :posts, force: true do |t|
t.datetime :reviewed_at
t.string :title, null: false
end
end
# requires #reviewed_at:datetime column
module Reviewable
extend ActiveSupport::Concern
included do
scope :reviewed, -> { where.not(reviewed_at: nil) }
scope :unreviewed, -> { where(reviewed_at: nil) }
end
def reviewed?
reviewed_at.present?
end
def review(time = DateTime.current)
update(reviewed_at: time)
end
end
class Post < ActiveRecord::Base
include Reviewable
validates_presence_of :title
end
# -- RAILS SPEC HELPER --
module InMemoryDatabaseHelpers
extend ActiveSupport::Concern
class_methods do
def switch_to_SQLite(&block)
before(:all) { switch_to_in_memory_database(&block) }
after(:all) { switch_back_to_test_database }
end
end
private
def switch_to_in_memory_database(&block)
raise 'No migration given' unless block_given?
ActiveRecord::Migration.verbose = false
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
ActiveRecord::Schema.define(version: 1, &block)
end
def switch_back_to_test_database
ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['test'])
end
end
# -- RAILS SPEC SUITE --
# requires a :reviewable object
shared_examples 'reviewable'do
describe 'the interface' do
subject { described_class.new }
it 'has the correct interface' do
expect(subject).to respond_to(:reviewed?)
expect(subject).to respond_to(:review)
expect(described_class).to respond_to(:reviewed)
expect(described_class).to respond_to(:unreviewed)
end
end
describe 'scopes' do
let!(:records) { described_class.where(id: [reviewed, unreviewed]) }
before do
expect(reviewed.reviewed?).to eql true
expect(unreviewed.reviewed?).to eql false
end
describe '.reviewed' do
subject { records.reviewed }
it { is_expected.to eq [reviewed] }
end
describe '.unreviewed' do
subject { records.unreviewed }
it { is_expected.to eq [unreviewed] }
end
end
describe '#reviewed?' do
subject { described_class.new }
it 'returns the correct boolean based on #reviewed_at' do
subject.reviewed_at = nil
expect(subject.reviewed?).to eql false
subject.reviewed_at = DateTime.current
expect(subject.reviewed?).to eql true
end
end
describe '#review' do
let(:time) { DateTime.current }
subject { unreviewed.review(time) }
it 'updates the reviewed_at attribute' do
expect { subject }.to change { unreviewed.reload.reviewed_at }.from(nil).to(time)
end
end
end
class FakeReviewable < ActiveRecord::Base
include Reviewable
end
describe Reviewable do
include InMemoryDatabaseHelpers
switch_to_SQLite do
create_table :fake_reviewables do |t|
t.datetime :reviewed_at
end
end
describe FakeReviewable, type: :model do
include_examples 'reviewable' do
let(:reviewed) { FakeReviewable.create! reviewed_at: DateTime.current }
let(:unreviewed) { FakeReviewable.create! reviewed_at: nil }
end
end
end
# This example is at the end to show that the Rails app went back to the current testing database configuration defined in the setup
describe Post do
include_examples 'reviewable' do
let(:reviewed) { Post.create! title: 'title1', reviewed_at: DateTime.current }
let(:unreviewed) { Post.create! title: 'title2', reviewed_at: nil }
end
end
@beydogan
Copy link

Thanks for providing the examples! I find it funny that "The Rails Way" praises using concerns yet there is no easy way to test it. You can see it in Basecamp's own Campfire code that they have concerns yet their tests are coupled to models directly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment