11

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 ]>
Brad Rice
  • 1,334
  • 2
  • 17
  • 36

2 Answers2

5

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.

Peter Alfvin
  • 28,599
  • 8
  • 68
  • 106
  • Thanks for the response, but this just returns the initial `MyClass.all` relation instead of my stubbed response. Should I be stubbing a different method? – Brad Rice Jan 28 '14 at 01:06
  • When you say "this" just returns the initial `MyClass.all`, what exactly are you referring to? – Peter Alfvin Jan 28 '14 at 03:18
  • Sorry, when my tests run, calling `MyClass.where(args args args)` it's returning the collection initially returned from `MyClass.all`, not the stubbed collection you wrote on line 2. Is `:[]` the right method? – Brad Rice Jan 28 '14 at 05:20
  • It returns the `MyClass.all` relation with the `:[]` method stubbed, which is what your question said it wanted to do. If you send the `:[]` method to that object (i.e. as my answer said you needed to do), you'll get the array you indicated. As to whether this is the "right" method to stub, I can't answer that for you as I don't know all the different ways in which you want the Relation to function. – Peter Alfvin Jan 28 '14 at 11:39
  • Hmm. Well that is not how this code is behaving for me. I copied the lines into my editor, changed to my class name and ran the tests. It's returning all the records, not my restricted collection. – Brad Rice Jan 28 '14 at 22:35
  • Please post the code you're running and the results you're getting. – Peter Alfvin Jan 29 '14 at 00:07
  • Ok, you're not calling the `[]` method on the `where` result, so you're stub isn't taking effect. If you said `subject['whatever']` in your spec, you'd get the custom array back. If you want `where` itself to return the custom array, you can do that, but then it won't be a relation - it will be an array. – Peter Alfvin Jan 29 '14 at 01:12
  • Ok. I was mainly curious if this was even possible, and it seems to not be (according to what you just said). Looks like I'll have to suck it up and create a test database after all. Thanks for your help – Brad Rice Jan 30 '14 at 01:06
2

Update 2022

The previous upvoted answer is wholly incorrect since does not work with indexing, .to_a, .first, .last, .any?, .none?, any pretty much every other method.

Instead, you can mock the records contained within a relation by stubbing its records method.

custom_records = ["a", "b", "c"]

relation = Model.all
relation.stub(:records).and_return(custom_records)

allow(Model).to receive(:where).and_return(relation)

# Later ...

records = Model.where('1 + 1 = 2') # content of the query doesn't matter, .where is mocked
records.first # => "a"
records.last # => "c"
records.to_a # => ["a", "b", "c"]
records.any? { |x| x == "b" } # => true

Most of the methods will work, but there are a few exceptions that will need to be stubbed separately.

  • .count - directly invokes a SELECT COUNT(*) SQL query, which bypasses our records mock. Fix:
    relation.stub(:count).and_return(custom_records.count)
    
  • .exists? - directly invokes another SQL query, again bypassing our records mock. Fix:
    relation.stub(:exists?).and_return(custom_records.present?)
    
  • others - There are probably other methods that you might need to stub (depending on if your code uses those methods), you can stub each method as-needed

Furthermore you can mock the return value of a has_many relation (which was my actual use case when googling this question) by doing

allow(record).to receive(:related_records).and_wrap_original do |original, *args, &block|
  relation = original.call(*args, &block)
  relation.stub(:records).and_return(my_custom_array_of_related_records)
  relation
end
Gaberocksall
  • 359
  • 2
  • 13