Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue Mocking Prepended Class Methods #6

Open
Rtwena opened this issue Oct 17, 2019 · 7 comments
Open

Issue Mocking Prepended Class Methods #6

Rtwena opened this issue Oct 17, 2019 · 7 comments

Comments

@Rtwena
Copy link

Rtwena commented Oct 17, 2019

Ruby: 2.5.3
Prependers: 0.1.1
Rails: 5.2.2

Given the follow file

# app/prependers/models/spree/product/scopes.rb

module Spree::Product::Scopes
  def self.prepended(base)
    base.singleton_class.prepend ClassMethods
  end

  module ClassMethods
    def purchasable
       # some code      
    end
  end
end

When trying to mock Spree::Product.purchasable

  before do
      allow(Spree::Product).to receive_messages(purchasable: purchasable_products)
  end

The follow error is returned AFTER the test has ran

     NameError:
       undefined method `purchasable' for class `#<Class:0x000055b4b3dc3198>'
     # /home/richard/.rvm/gems/ruby-2.5.3/gems/rspec-mocks-3.8.0/lib/rspec/mocks/method_double.rb:108:in `public'
     # /home/richard/.rvm/gems/ruby-2.5.3/gems/rspec-mocks-3.8.0/lib/rspec/mocks/method_double.rb:108:in `restore_original_visibility'
     # /home/richard/.rvm/gems/ruby-2.5.3/gems/rspec-mocks-3.8.0/lib/rspec/mocks/method_double.rb:87:in `restore_original_method'
     # /home/richard/.rvm/gems/ruby-2.5.3/gems/rspec-mocks-3.8.0/lib/rspec/mocks/method_double.rb:118:in `reset'
     # /home/richard/.rvm/gems/ruby-2.5.3/gems/rspec-mocks-3.8.0/lib/rspec/mocks/proxy.rb:319:in `block in reset'
     # /home/richard/.rvm/gems/ruby-2.5.3/gems/rspec-mocks-3.8.0/lib/rspec/mocks/proxy.rb:319:in `each_value'
     # /home/richard/.rvm/gems/ruby-2.5.3/gems/rspec-mocks-3.8.0/lib/rspec/mocks/proxy.rb:319:in `reset'
     # /home/richard/.rvm/gems/ruby-2.5.3/gems/rspec-mocks-3.8.0/lib/rspec/mocks/space.rb:79:in `block in reset_all'
     # /home/richard/.rvm/gems/ruby-2.5.3/gems/rspec-mocks-3.8.0/lib/rspec/mocks/space.rb:79:in `each_value'
     # /home/richard/.rvm/gems/ruby-2.5.3/gems/rspec-mocks-3.8.0/lib/rspec/mocks/space.rb:79:in `reset_all'
     # /home/richard/.rvm/gems/ruby-2.5.3/gems/rspec-mocks-3.8.0/lib/rspec/mocks.rb:52:in `teardown'
     # /home/richard/.rvm/gems/ruby-2.5.3/gems/rspec-core-3.8.0/lib/rspec/core/mocking_adapters/rspec.rb:27:in `teardown_mocks_for_rspec'

When trying to mock via a class_double

    let(:spree_product) { class_double Spree::Product, purchasable: purchasable_products }

The follow error is returned During other tests that are using Spree::Product

  #<ClassDouble(Spree::Product) (anonymous)> was originally created in one example but has leaked into another example and can no longer be used. rspec-mocks' doubles are designed to only last for one example, and you need to create a new one in each example you wish to use it for.

When trying to force reset the mock using

after do
  RSpec::Mocks.space.proxy_for(Spree::Product).reset
end

The follow error is returned AFTER the test has ran

WARNING: RSpec could not fully restore Spree::Product.purchasable, possibly because the method has been redefined by something outside of RSpec. Called from /spec/interactors/concerns/~omitted~/~ommitted~_spec.rb:29:in `block (3 levels) in <top (required)>'.

NameError: undefined method `purchasable' for class `#<Class:0x0000563d345f48e0>'

Maybe related to rspec/rspec-mocks#1218

@Rtwena
Copy link
Author

Rtwena commented Oct 17, 2019

The above issues can be mitigated by using
base.extend ClassMethods instead of singleton_class.prepend

However, with base.extend you cannot overwrite existing class methods.
You will have to use singleton_class.prepend

@aldesantis
Copy link
Member

@Rtwena I tried to reproduce the issue with the following spec:

require 'spec_helper'

RSpec.describe 'Mocking' do
  before(:all) do
    module PrependersTest
      class BaseClass; end
    end

    module PrependersTest::AddClassMethod
      include Prependers::Prepender.new

      module ClassMethods
        def new_method; end
      end
    end
  end

  before do
    allow(PrependersTest::BaseClass).to receive_messages(new_method: 'test')
  end

  it 'works when mocking a class method' do
    expect(PrependersTest::BaseClass.new_method).to eq('test')
  end
end

However, the test passes with no errors being returned either during or after the test.

Do you have a Git repo I can use to reproduce the issue?

@Rtwena
Copy link
Author

Rtwena commented Jun 4, 2020

I'm going to close this issue and reopen when I have actual proof/specs.

@Rtwena Rtwena closed this as completed Jun 4, 2020
@vassalloandrea
Copy link
Member

vassalloandrea commented Oct 12, 2021

Found a similar bug:

app/prependers/controllers/spree/taxons_controller/add_autocomplete.rb

# frozen_string_literal: true

module Spree::TaxonsController::AddAutocomplete
  def autocomplete
    render json: ::Spree::Taxon.autocomplete(query: params[:keywords] || nil)
  end
end

app/prependers/models/spree/taxon/add_searchkick.rb

# frozen_string_literal: true

module Spree::Taxon::AddSearchkick
  def self.prepended(base)
    base.searchkick suggest: [:name], word_start: [:name]
  end

  def search_data
    {
      name: name&.strip,
      permalink: permalink,
      parent_name: parent&.name&.strip
    }
  end

  module ClassMethods
    def autocomplete(query:)
      search(
        query,
        limit: 10,
        load: false,
        misspellings: { below: 3 },
      ).map do |result|
        {
          name: result.name,
          permalink: result.permalink,
          parent_name: result.parent_name,
        }
      end.uniq
    end
  end
end

Error running spec/requests/spree/taxons_controller_spec.rb:

# frozen_string_literal: true

require 'rails_helper'

RSpec.describe Spree::TaxonsController, type: :request do
  describe 'GET /autocomplete/taxons' do
    subject(:req) { ->(keywords) { get(spree.taxons_autocomplete_path(keywords: keywords)) } }

    before do
      allow(::Spree::Taxon).to receive(:autocomplete)
    end

    it 'calls the autocomplete method with the passed keywords' do
      req.call('Pump')
      expect(Spree::Taxon).to have_received(:autocomplete).with(query: 'Pump')
    end

    it 'calls the autocomplete method with the passed keywords' do
      req.call(nil)
      expect(Spree::Taxon).to have_received(:autocomplete).with(query: nil)
    end
  end
end

image

@nerfologist
Copy link

nerfologist commented Nov 5, 2021

Hi, I am experiencing a similar issue. My code prepended to Spree::Carton:

module Cometeer
  module Spree
    module Carton
      module AddNeedsDryIce
        def needs_dry_ice?
          self.class.needs_dry_ice.exists?(id: id)
        end

        module ClassMethods
          def needs_dry_ice
            # some business logic
          end
        end
      end
    end
  end
end

The prepended class method working in the console:

cometeer:development(main):001:0> Spree::Carton.needs_dry_ice
  Spree::Carton Load (20.5ms)  SELECT DISTINCT "spree_cartons".* FROM "spree_cartons" ...
=> #<ActiveRecord::Relation [#<Spree::Carton id: 2, number: "C03108044687" ...

In my test:

allow(::Spree::Carton).to receive(:needs_dry_ice) { ::Spree::Carton.all }

When running the test:

NameError:
    undefined method `needs_dry_ice' for class `#<Class:0x00007fa771d8c690>'

@aldesantis
Copy link
Member

Interesting... This will require digging deep into the internals of RSpec's verify_partial_doubles to understand why it thinks the method doesn't exist. 😩 I'll try to take a look next week and let y'all know!

@FrancescoAiello01
Copy link

FrancescoAiello01 commented Jun 24, 2022

I ran into the problem with basically the same setup as @nerfologist, just wanted to keep this thread alive ☠️

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants