rspec / rspec/rspec

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

Open
#67 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Ruby
Stars
117
Forks
51
Avg merge
59m
Merged PRs (30d)
3

Description

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

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with lib/rspec/mocks/proxy.rb at line 228 and reproduce the supplied Shell.start example under Ruby 2.7. Trace why the proxy calls the superclass method when keyword arguments are present. Done means the example completes without NoMethodError and reports the expected receive expectation.

Written by the indexing model from the issue text.

Assessment

Tech stack
ruby
Domain
testing-qa
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Stale
Clarity
Clearly specified
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.