avjaarsveld
avjaarsveld

Reputation: 599

Rails Engine host application classes in RSpec tests

I have a Rails Engine that includes methods that link to some of the host application's classes (I know this type of coupling is bad, but in this case it is unavoidable).

I need to test methods that use the host's classes, but I get a uninitialized constant MyEngine::BaseUser error when trying to double/mock/stub the host's classes (BaseUser or Tutor in this case).

I have had a stab at getting round this problem by creating mock classes, but I think what I've left with is a bad idea and means my tests are less useful (see below).

Any idea what I could do better, or suggestions for a better direction to go in?

As I said above, I got round this (badly) like this:

BaseUser = Class.new do
    attr_accessor :id

    def initialize(id = 1)
        @id = id
    end

    def self.find(id)
        self.new(id)
    end

    def tutor
        Tutor.find(self.id)
    end
end

class Tutor
    attr_accessor :id, :first_name

    def initialize(id = 1)
        @id = id
        @first_name = "Tutor with ID #{id}'s first name"
    end

    def self.find(id)
        self.new(id)
    end
end

it 'returns the hosts first name' do
    allow(MyEngine).to receive_message_chain(:user_class, :constantize) { BaseUser }
    ai = FactoryGirl.create(:availability_interval, host_id: 1)
    expect(ai.host_first_name).to eq BaseUser.find(1).tutor.first_name
end

The method I am testing looks like this:

def host_full_name
    MyEngine.user_class.constantize.find(self.host_id).tutor.full_name
end

(MyEngine.user_class is "BaseUser")

Upvotes: 0

Views: 285

Answers (2)

avjaarsveld
avjaarsveld

Reputation: 599

What I did in the question still seems to be the best solution, so I am going ahead and answering this question with another example of this approach.

Here I needed to mock/model a Setting that was used like this in the code (outside of the Engine): my_value = Setting.find_by_name('SETTING_NAME').value

describe 'ratable_based_on_time' do

    Setting = Class.new do
        def self.value
        end
        def self.find_by_name(name)
        end
    end

    before(:each) do
        allow(Setting).to receive_message_chain(:find_by_name, :value).and_return("30")
    end

    let(:availability_interval) { FactoryGirl.create(:availability_interval) }

    it 'should return availability_intervals that are rateable based on time since start' do
        availability_interval.update_column(:start_time, 1.hour.ago)
        availability_interval.reload
        expect(AvailabilityInterval.ratable_based_on_time).to eq [availability_interval]
    end

    it 'should not return availability_intervals that are not rateable based on time since start' do
        availability_interval.update_column(:start_time, 25.minutes.ago)
        availability_interval.reload
        expect(AvailabilityInterval.ratable_based_on_time).to eq []
    end
end

Upvotes: 0

Taryn East
Taryn East

Reputation: 27747

Your engine namespaces everything to your engine. If you are trying access a class that's actually defined in the base scope (ie outside of your engine), you can force it to find that class in the base scope with :: eg:

 expect(ai.host_first_name).to eq ::BaseUser.find(1).tutor.first_name

Upvotes: 1

Related Questions