Running into an issue in Rspec. Let's say I have this:
class Book; has_many: :pages; end
class Page; belongs_to: :book; end
describe Pages
let(:book) { create(:book) }
let(:page) { create(:page, book: book) }
before do
allow(page).to receive(:last_page?).and_return(last_page)
book.pages << page
end
context "weird behavior" do
let(:last_page) { "Orange" }
it do
# these two pass as expected
expect(book.pages.first).to eq page # passes, as expected
expect(book.pages.first.last_page?).to eq "Orange" # passes, as expected
# this is where weird things happen
expect(book.pages.order("created_at DESC").first).to eq page # passes, as expected
expect(book.pages.order("created_at DESC").first.last_page?).to eq "Orange" # will fail and return the actual method call
end
end
end
Why does ".order("created_at DESC")" mess up the "allow" statement, even though the actual objects are still equal?
CodePudding user response:
I can step through this.
describe Pages
let(:book) { create(:book) }
let(:page) { create(:page, book: book) }
before do
# here you create the expectation on the page object
allow(page).to receive(:last_page?).and_return(last_page)
# and append the page to the pages collection
# this collection / scope could be considered to be already loaded from the DB if you like
book.pages << page
end
context "weird behavior" do
let(:last_page) { "Orange" }
it do
# these two pass as expected
# and here you are simply extracting the page object back from what is effectively a loaded pages scope.
expect(book.pages.first).to eq page # passes, as expected
expect(book.pages.first.last_page?).to eq "Orange" # passes, as expected
# it's equivalent to
expect(page).to eq page # passes, as expected
expect(page.last_page?).to eq "Orange" # passes, as expected
# I would expect this to fail since the first page would be different to the object you created the expectation on
expect(book.pages.reload.first).to eq page # passes, as expected
expect(book.pages.reload.first.last_page?).to eq "Orange" # passes, as expected
# this is where weird things happen
# not so weird.
# book.pages is one scope object
# book.pages.limit(1) would be another
# book.pages.order('id') would be another
# book.pages.order('created_at DESC') would fetch from the database and that object would not have your expectation on it
# expections can only be made on an object not on every instance of an object with that id.
expect(book.pages.order("created_at DESC").first).to eq page # passes, as expected
expect(book.pages.order("created_at DESC").first.last_page?).to eq "Orange" # will fail and return the actual method call
end
end
end
CodePudding user response:
Here's what happens:
book.pages.first.last_page?
first
returns the object as you set up in the before block:
book.pages << page
So when you run allow(page)
and book.pages.first
they point to the same object, try
page.object_id == book.pages.first.object_id # true
But when you call
book.pages.order("created_at DESC").first
Rails is fetching all related pages from the database and builds completely new objects, so
page.object_id == book.pages.order("created_at DESC").first.object_id # false
So the mocked object page
is a different object that book.pages.order("created_at DESC").first.object_id
and the latter just calls the original method last_page?
implemented on the Page model.
To put it in other words: your mock is for this specific object in memory, and calling order
fetches records for DB and creates different objects in memory, and you don't have mocking set up on those.
There are many ways to fix it (better or worse depending on some other specificities of your system you're designing), and this deserves a whole separate Stack Overflow question IMHO (feel free to post one!). But I hope it's clear why one of your expectations passes and the other one fails.