How can I test methods in the ApplicationRecord abstract base class?
Asked Answered
C

3

9

I haven't found a good way to test ApplicationRecord methods.

Let's say I have a simple method named one:

class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true

  def one
    1
  end
end

And I want to test it:

describe ApplicationRecord do
  let(:it) { described_class.new }

  it 'works' do
    expect(it.one).to eq 1
  end
end

This dies, unsurprisingly, with NotImplementedError: ApplicationRecord is an abstract class and cannot be instantiated.

So I tried the anonymous class suggestion in Testing abstract classes in Rspec:

let(:it) { Class.new(described_class).new }

And this dies with TypeError: no implicit conversion of nil into String, presumably because the record's table name is nil.

Can anyone suggest a nice, simple way to test ApplicationRecord methods? Hopefully one that doesn't introduce dependencies on other classes in my application and doesn't root around in ActiveRecord internals?

Cartan answered 12/2, 2016 at 22:54 Comment(0)
P
9

This has been working for me in our tests:

class TestClass < ApplicationRecord
  def self.load_schema!
    @columns_hash = {}
  end
end

describe ApplicationRecord do
  let(:record) { TestClass.new }

  describe "#saved_new_record?" do
    subject { record.saved_new_record? }

    before { allow(record).to receive(:saved_change_to_id?).and_return(id_changed) }

    context "saved_change_to_id? = true" do
      let(:id_changed) { true }

      it { is_expected.to be true }
    end

    context "saved_change_to_id? = false" do
      let(:id_changed) { false }

      it { is_expected.to be false }
    end
  end
end

It just prevents the class from attempting a database connection to load the table schema.

Obviously as Rails moves along you might have to update the way you do this but at least it is located in one easy to find place.

I prefer this much more than having another module just to allow testing.

Predestine answered 26/1, 2018 at 18:51 Comment(0)
I
6

I would suggest extracting those methods into module (concern) and leave ApplicationRecord alone.

module SomeCommonModelMethods
  extend ActiveSupport::Concern

  def one
    1
  end
end

class ApplicationRecord < ActiveRecord::Base
  include SomeCommonModelMethods
  self.abstract_class = true
end

describe SomeCommonModelMethods do
  let(:it) { Class.new { include SomeCommonModelMethods }.new } } 

  it 'works' do
    expect(it.one).to eq 1
  end
end
Islean answered 13/2, 2016 at 19:14 Comment(3)
Yes! Personally, I avoid concerns (I think there's some good debate on this). But, I agree with staying away from abstract_class on ActiveRecord.Geraldgeralda
Creating a concern file for a one-line function? That feels like it's contorting my app's source code to make up for a language/library limitation. I'm still hoping someone can suggest how to test the ApplicationRecord class itself but thank you for the very well written suggestion!Cartan
ApplicationRecord is the standard in Rails 5. Why would anyone avoid it?Rhinarium
R
1

If you're using Rspec, then you can create a shared example and then call it from the spec of each model that inherits from ApplicationRecord. This will have the disadvantage of testing all of that behavior on every model, but the overhead should be fairly low unless you're cramming a lot of shared behavior into ApplicationRecord.

Rhinarium answered 19/4, 2018 at 23:46 Comment(1)
Unfortunately, the link is broken. Heroku says "Application error."Grindlay

© 2022 - 2024 — McMap. All rights reserved.