Home > Back-end >  Why does ".order("created_at DESC")" mess up "allow" in Rspec?
Why does ".order("created_at DESC")" mess up "allow" in Rspec?

Time:11-09

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.

  • Related