Reputation: 1344
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 ]>
Upvotes: 11
Views: 22973
Reputation: 835
The previous upvoted answer is wholly incorrect since does not work with indexing, .to_a
, .first
, .last
, .any?
, .none?
, and most other methods.
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?)
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
Upvotes: 3
Reputation: 29429
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.
Upvotes: 6