Stub ActiveRecord::Relation with ActiveRecord objects

Brad Rice picture Brad Rice · Jan 25, 2014 · Viewed 14.4k times · Source

I'm not testing a Rails app. Just getting that out of the way.

I'm testing a library that connects to a relatively active server, restricting records by timestamp. These returned records change as time goes on, making testing other restrictions more complicated. I need to stub out the ActiveRecord::where method to return my own custom relation with objects I create to meet the criteria I need.

Something like

relation = double(ActiveRecord::Relation)
relation.stub(:[]).and_return( [MyClass.new(...), MyClass.new(...), ...] )
MyClass.stub(:where).and_return( relation )

is what I'd like, but that doesn't work. I need it to be an ActiveRecord::Relation because I need to be able to call ActiveRecord::where and ActiveRecord::select on the object in the code.


Edit 2014-01-28

In lib/call.rb

class Call < ActiveRecord::Base
  class << self
    def sales start_time, end_time
      restricted_records = records(start_time, end_time, :agent_id)
      #other code
    end

    #other methods

    private

      def records start_time, end_time, *select
        # I'm leaving in commented code so you can see why I want the ActiveRecord::Relation object, not an Array
        calls = Call.where("ts BETWEEN '#{start_time}' AND '#{end_time}'") #.select(select)
        raise calls.inspect
          #.to_a.map(&:serializable_hash).map {|record| symbolize(record)}
      end
  end
end

In spec/call_spec.rb

require 'spec_helper'
require 'call.rb'

describe Call do
  let(:period_start) { Time.now - 60 }
  let(:period_end) { Time.now }

  describe "::sales" do
    before do
      relation = Call.all
      relation.stub(:[]).and_return( [Call.new(queue: "12345")] )
      Call.stub(:where).and_return( relation )
    end

    subject { Call.sales(period_start, period_end) }

    it "restricts results to my custom object" do
      subject
    end
  end
end

Output from test:

RuntimeError:
  #<ActiveRecord::Relation [ #an array containing all the actual Call records, not my object ]>

Answer

Peter Alfvin picture Peter Alfvin · Jan 26, 2014

ActiveRecord::Relation is a class and :[] is an instance method of that class. You're stubbing a method of the class itself, so it's not going to be invoked by any of the Rails code.

If you want MyClass.where to return a relation with just the :[] stubbed, you'll have to create a Relation instance first, as in:

relation = MyClass.all
relation.stub(:[]).and_return( [MyClass.new(...), MyClass.new(...), ...] )
MyClass.stub(:where).and_return( relation )

However, note that in order to get to your returned array in this context, you'll need to do:

MyClass.where("ignored parameters")["ignored parameters"]

Further, if you subsequently call where on relation, you'll return a new instance of Relation which will no longer be stubbed.