Search code examples
ruby-on-railsrspecmethod-missing

Rspec - Argument error after overwriting method_missing and respond_to_missing


I have a controller that i want to write rspec for

results_controller.rb

 class Api::V1::ResultsController < Api::V1::ApplicationController
   before_action :devices
   include DataHelper

  def show
    results = get_dr_results
    render json: { data: results }
  end
  
  private

  def get_dr_results
    program_ids = method_defined_in_crucible_helper
  end
end

module DataHelper
  include Cruciblehelper
  
 def method_missing(method_name, *args, &block)
   if condition
    do_something  
  else
    super.method_missing(method_name, *args, &block)
  end
end

def respond_to_missing?
  true
end
end

module CrucibleHelper
  def method_defined_in_crucible_helper
  end
end

Now in my rspec, I try to mock the method method_defined_in_crucible_helper.

describe Api::V1::DrResultsController, type: :controller do
  describe 'GET #show' do
    before do
    allow_any_instance_of(CrucibleHelper).to receive(:method_defined_in_crucible_helper) { [utility_program.id, utility_program2.id] }
  end

  context 'returns data' do
    context 'returns expected events' do
      it 'should return success response with expected events' do
        get :show
      expect(JSON.parse(response.body)).to eq(expected_response)
    end
  end

I am getting

     Failure/Error:
   def respond_to_missing?
     true
   end
 
 ArgumentError:
   wrong number of arguments (given 2, expected 0)
 # ./app/helpers/data_helper.rb:72:in `respond_to_missing?'

If I comment out respond_to_missing? method, then my specs are executing OK. Can someone help me in fixing this error?


Solution

  • Ruby Delegator#respond_to_missing? is method take responsible for returning whether a missing method be able to handled by the object or not, it takes 2 parameters: the missing method name and the option include_private.

    The best practice is: always define respond_to_missing? when overriding method_missing.

    However i do not prefer the way you applied, the reason behind that is The Rule of Least Surprise, take a look:

    class DataHelper
     def method_missing(method_name, *args, &block)
       if method_name.to_s.start_with?('delegate')
        puts "a delegate method"
       else
        super
       end
     end
    
     def respond_to_missing?(method_name, include_private = false)
       true
     end
    end
    
    d = DataHelper.new
    d.respond_to?(:answer) # true
    d.answer # `method_missing': undefined method `answer' ... SURPRISE
    

    as you can see, d response that he can responsible for the answer method but when call that method, a method_missing error be raised.

    So, you need to make both method_missing and respond_to_missing? match together:

    class DataHelper
     def method_missing(method_name, *args, &block)
       if can_handle?(method_name)
        puts "a delegate method"
       else
        super
       end
     end
    
     def respond_to_missing?(method_name, include_private = false)
       return true if can_handle?(method_name)
       super
     end
     
     private
    
     def can_handle?(method_name)
       method_name.to_s.start_with?('delegate')
     end
    end
    
    d = D.new
    d.respond_to?(:delegate_answer) # true
    d.delegate_answer # delegate method
    d.respond_to?(:answer) # false
    d.answer # error