rspec-mocks icon indicating copy to clipboard operation
rspec-mocks copied to clipboard

Stub attempts to call method on Class'es superclass when method accepts keyword arguments on Ruby 2.7

Open postmodern opened this issue 2 years ago • 1 comments

Subject of the issue

If you rspec stubs to test whether a class method, which accepts both positional and keyword arguments, is called with a positional argument, then RSpec::Mocks::Proxy will attempt to call the superclass's method. This will result in a confusing error (undefined method 'start' for Object:Class) if the Class in question does not inherit from another Class. This bug occurs under ruby-2.7, but not ruby-3.x. However, the bug does not occur if I remove **kwargs from the method definitions.

Your environment

  • Ruby version: ruby 2.7.5p203 (2021-11-24 revision f69aeb8314) [x86_64-linux]
  • rspec-mocks version: 3.11.1

Steps to reproduce

require 'rspec'

class Shell
  def self.start(io,**kwargs)
  end
end

class IO
  def shell(**kwargs)
    Shell.start(self,**kwargs)
  end
end

describe IO do
  subject { File.new(__FILE__) }

  describe "#shell" do
    it do
      expect(Shell).to receive(:start).with(subject)

      subject.shell
    end
  end
end

Rescuing NoMethodError in the test points us to RSpec::Mocks::Proxy line 228.

/home/postmodern/.gem/ruby/2.7.5/gems/rspec-mocks-3.11.1/lib/rspec/mocks/proxy.rb:228:in `message_received'
/home/postmodern/.gem/ruby/2.7.5/gems/rspec-mocks-3.11.1/lib/rspec/mocks/proxy.rb:366:in `message_received'
/home/postmodern/.gem/ruby/2.7.5/gems/rspec-mocks-3.11.1/lib/rspec/mocks/method_double.rb:80:in `proxy_method_invoked'
/home/postmodern/.gem/ruby/2.7.5/gems/rspec-mocks-3.11.1/lib/rspec/mocks/method_double.rb:64:in `block (2 levels) in define_proxy_method'
/home/postmodern/test/ruby/rspec/spec/test_spec.rb:10:in `shell'
/home/postmodern/test/ruby/rspec/spec/test_spec.rb:22:in `block (3 levels) in <top (required)>'
/home/postmodern/.gem/ruby/2.7.5/gems/rspec-core-3.11.0/lib/rspec/core/example.rb:263:in `instance_exec'
/home/postmodern/.gem/ruby/2.7.5/gems/rspec-core-3.11.0/lib/rspec/core/example.rb:263:in `block in run'
/home/postmodern/.gem/ruby/2.7.5/gems/rspec-core-3.11.0/lib/rspec/core/example.rb:511:in `block in with_around_and_singleton_context_hooks'
/home/postmodern/.gem/ruby/2.7.5/gems/rspec-core-3.11.0/lib/rspec/core/example.rb:468:in `block in with_around_example_hooks'
/home/postmodern/.gem/ruby/2.7.5/gems/rspec-core-3.11.0/lib/rspec/core/hooks.rb:486:in `block in run'
/home/postmodern/.gem/ruby/2.7.5/gems/rspec-core-3.11.0/lib/rspec/core/hooks.rb:624:in `run_around_example_hooks_for'
/home/postmodern/.gem/ruby/2.7.5/gems/rspec-core-3.11.0/lib/rspec/core/hooks.rb:486:in `run'
/home/postmodern/.gem/ruby/2.7.5/gems/rspec-core-3.11.0/lib/rspec/core/example.rb:468:in `with_around_example_hooks'
/home/postmodern/.gem/ruby/2.7.5/gems/rspec-core-3.11.0/lib/rspec/core/example.rb:511:in `with_around_and_singleton_context_hooks'
/home/postmodern/.gem/ruby/2.7.5/gems/rspec-core-3.11.0/lib/rspec/core/example.rb:259:in `run'
/home/postmodern/.gem/ruby/2.7.5/gems/rspec-core-3.11.0/lib/rspec/core/example_group.rb:646:in `block in run_examples'
/home/postmodern/.gem/ruby/2.7.5/gems/rspec-core-3.11.0/lib/rspec/core/example_group.rb:642:in `map'
/home/postmodern/.gem/ruby/2.7.5/gems/rspec-core-3.11.0/lib/rspec/core/example_group.rb:642:in `run_examples'
/home/postmodern/.gem/ruby/2.7.5/gems/rspec-core-3.11.0/lib/rspec/core/example_group.rb:607:in `run'
/home/postmodern/.gem/ruby/2.7.5/gems/rspec-core-3.11.0/lib/rspec/core/example_group.rb:608:in `block in run'
/home/postmodern/.gem/ruby/2.7.5/gems/rspec-core-3.11.0/lib/rspec/core/example_group.rb:608:in `map'
/home/postmodern/.gem/ruby/2.7.5/gems/rspec-core-3.11.0/lib/rspec/core/example_group.rb:608:in `run'
/home/postmodern/.gem/ruby/2.7.5/gems/rspec-core-3.11.0/lib/rspec/core/runner.rb:121:in `block (3 levels) in run_specs'
/home/postmodern/.gem/ruby/2.7.5/gems/rspec-core-3.11.0/lib/rspec/core/runner.rb:121:in `map'
/home/postmodern/.gem/ruby/2.7.5/gems/rspec-core-3.11.0/lib/rspec/core/runner.rb:121:in `block (2 levels) in run_specs'
/home/postmodern/.gem/ruby/2.7.5/gems/rspec-core-3.11.0/lib/rspec/core/configuration.rb:2068:in `with_suite_hooks'
/home/postmodern/.gem/ruby/2.7.5/gems/rspec-core-3.11.0/lib/rspec/core/runner.rb:116:in `block in run_specs'
/home/postmodern/.gem/ruby/2.7.5/gems/rspec-core-3.11.0/lib/rspec/core/reporter.rb:74:in `report'
/home/postmodern/.gem/ruby/2.7.5/gems/rspec-core-3.11.0/lib/rspec/core/runner.rb:115:in `run_specs'
/home/postmodern/.gem/ruby/2.7.5/gems/rspec-core-3.11.0/lib/rspec/core/runner.rb:89:in `run'
/home/postmodern/.gem/ruby/2.7.5/gems/rspec-core-3.11.0/lib/rspec/core/runner.rb:71:in `run'
/home/postmodern/.gem/ruby/2.7.5/gems/rspec-core-3.11.0/lib/rspec/core/runner.rb:45:in `invoke'
/home/postmodern/.gem/ruby/2.7.5/gems/rspec-core-3.11.0/exe/rspec:4:in `<top (required)>'
/home/postmodern/.gem/ruby/2.7.5/bin/rspec:23:in `load'
/home/postmodern/.gem/ruby/2.7.5/bin/rspec:23:in `<main>'

Expected behavior

IO
  #shell
    is expected to receive start(#<File:/home/postmodern/test/ruby/rspec/spec/test_spec.rb>) 1 time

Finished in 0.00547 seconds (files took 0.08959 seconds to load)
1 example, 0 failures

Actual behavior

IO
  #shell
    is expected to receive start(#<File:/home/postmodern/test/ruby/rspec/spec/test_spec.rb>) 1 time (FAILED - 1)

Failures:

  1) IO#shell is expected to receive start(#<File:/home/postmodern/test/ruby/rspec/spec/test_spec.rb>) 1 time
     Failure/Error: Shell.start(self,**kwargs)
     
     NoMethodError:
       undefined method `start' for Object:Class
     # ./spec/test_spec.rb:10:in `shell'
     # ./spec/test_spec.rb:21:in `block (3 levels) in <top (required)>'

Finished in 0.00563 seconds (files took 0.09395 seconds to load)
1 example, 1 failure

Failed examples:

rspec ./spec/test_spec.rb:18 # IO#shell is expected to receive start(#<File:/home/postmodern/test/ruby/rspec/spec/test_spec.rb>) 1 time

postmodern avatar May 07 '22 03:05 postmodern