rspec / rspec-mocks

RSpec's 'test double' framework, with support for stubbing and mocking
https://rspec.info
MIT License
1.16k stars 357 forks source link

undefined method `expectation_fulfilled?' for nil:NilClass #1496

Closed AntoineBecquet closed 1 year ago

AntoineBecquet commented 1 year ago

undefined method `expectation_fulfilled?' for nil:NilClass

When using allow_any_instance_of and/or expect_any_instance_of on two classes, one inheriting from the other, and having a failed expectation, this raise an error undefined method 'expectation_fulfilled?' for nil:NilClass.

Your environment

Steps to reproduce

# frozen_string_literal: true

begin
  require "bundler/inline"
rescue LoadError => e
  $stderr.puts "Bundler version 1.10 or later is required. Please update your Bundler"
  raise e
end

gemfile(true) do
  source "https://rubygems.org"

  gem "rspec", "3.12.0" # Activate the gem and version you are reporting the issue against.
end

puts "Ruby version is: #{RUBY_VERSION}"
require 'rspec/autorun'

class Foo
  def parent_method
  end
end

class Bar < Foo
  def child_method
  end
end

RSpec.describe Bar do
  it 'fails' do
    expect_any_instance_of(Foo).to receive(:parent_method)
    expect_any_instance_of(Bar).to receive(:child_method)
    Bar.new.child_method
  end
end

Expected behavior

Fails with the message : Exactly one instance should have received the following message(s) but didn't: parent_method

Actual behavior

Raise an error :

Using bundler 2.3.7
Using diff-lcs 1.5.0
Using rspec-support 3.12.0
Using rspec-core 3.12.0
Using rspec-expectations 3.12.0
Using rspec-mocks 3.12.0
Using rspec 3.12.0
Ruby version is: 3.1.2
F

Failures:

  1) Bar
     Failure/Error: method_name.to_s if ExpectationChain === chains.last unless chains.last.expectation_fulfilled?

     NoMethodError:
       undefined method `expectation_fulfilled?' for nil:NilClass

                   method_name.to_s if ExpectationChain === chains.last unless chains.last.expectation_fulfilled?
                                                                                          ^^^^^^^^^^^^^^^^^^^^^^^
# [FILTERED]/rspec-mocks-3.12.0/lib/rspec/mocks/any_instance/message_chains.rb:52:in `block in unfulfilled_expectations'
# [FILTERED]/rspec-mocks-3.12.0/lib/rspec/mocks/any_instance/message_chains.rb:51:in `each'
# [FILTERED]/rspec-mocks-3.12.0/lib/rspec/mocks/any_instance/message_chains.rb:51:in `map'
# [FILTERED]/rspec-mocks-3.12.0/lib/rspec/mocks/any_instance/message_chains.rb:51:in `unfulfilled_expectations'
# [FILTERED]/rspec-mocks-3.12.0/lib/rspec/mocks/any_instance/recorder.rb:100:in `verify'
# [FILTERED]/rspec-mocks-3.12.0/lib/rspec/mocks/space.rb:75:in `block in verify_all'
# [FILTERED]/rspec-mocks-3.12.0/lib/rspec/mocks/space.rb:75:in `each_value'
# [FILTERED]/rspec-mocks-3.12.0/lib/rspec/mocks/space.rb:75:in `verify_all'
# [FILTERED]/rspec-mocks-3.12.0/lib/rspec/mocks.rb:45:in `verify'
# [FILTERED]/rspec-core-3.12.0/lib/rspec/core/mocking_adapters/rspec.rb:23:in `verify_mocks_for_rspec'
# [FILTERED]/rspec-core-3.12.0/lib/rspec/core/example.rb:525:in `verify_mocks'
# [FILTERED]/rspec-core-3.12.0/lib/rspec/core/example.rb:519:in `run_after_example'
# [FILTERED]/rspec-core-3.12.0/lib/rspec/core/example.rb:283:in `block in run'
# [FILTERED]/rspec-core-3.12.0/lib/rspec/core/example.rb:511:in `block in with_around_and_singleton_context_hooks'
# [FILTERED]/rspec-core-3.12.0/lib/rspec/core/example.rb:468:in `block in with_around_example_hooks'
# [FILTERED]/rspec-core-3.12.0/lib/rspec/core/hooks.rb:486:in `block in run'
# [FILTERED]/rspec-core-3.12.0/lib/rspec/core/hooks.rb:624:in `run_around_example_hooks_for'
# [FILTERED]/rspec-core-3.12.0/lib/rspec/core/hooks.rb:486:in `run'
# [FILTERED]/rspec-core-3.12.0/lib/rspec/core/example.rb:468:in `with_around_example_hooks'
# [FILTERED]/rspec-core-3.12.0/lib/rspec/core/example.rb:511:in `with_around_and_singleton_context_hooks'
# [FILTERED]/rspec-core-3.12.0/lib/rspec/core/example.rb:259:in `run'
# [FILTERED]/rspec-core-3.12.0/lib/rspec/core/example_group.rb:646:in `block in run_examples'
# [FILTERED]/rspec-core-3.12.0/lib/rspec/core/example_group.rb:642:in `map'
# [FILTERED]/rspec-core-3.12.0/lib/rspec/core/example_group.rb:642:in `run_examples'
# [FILTERED]/rspec-core-3.12.0/lib/rspec/core/example_group.rb:607:in `run'
# [FILTERED]/rspec-core-3.12.0/lib/rspec/core/runner.rb:121:in `block (3 levels) in run_specs'
# [FILTERED]/rspec-core-3.12.0/lib/rspec/core/runner.rb:121:in `map'
# [FILTERED]/rspec-core-3.12.0/lib/rspec/core/runner.rb:121:in `block (2 levels) in run_specs'
# [FILTERED]/rspec-core-3.12.0/lib/rspec/core/configuration.rb:2070:in `with_suite_hooks'
# [FILTERED]/rspec-core-3.12.0/lib/rspec/core/runner.rb:116:in `block in run_specs'
# [FILTERED]/rspec-core-3.12.0/lib/rspec/core/reporter.rb:74:in `report'
# [FILTERED]/rspec-core-3.12.0/lib/rspec/core/runner.rb:115:in `run_specs'
# [FILTERED]/rspec-core-3.12.0/lib/rspec/core/runner.rb:89:in `run'
# [FILTERED]/rspec-core-3.12.0/lib/rspec/core/runner.rb:71:in `run'
# [FILTERED]/rspec-core-3.12.0/lib/rspec/core/runner.rb:45:in `invoke'
# [FILTERED]/rspec-core-3.12.0/lib/rspec/core/runner.rb:38:in `perform_at_exit'
# [FILTERED]/rspec-core-3.12.0/lib/rspec/core/runner.rb:24:in `block in autorun'

Additional information

It's coming from theses lines : https://github.com/rspec/rspec-mocks/blob/main/lib/rspec/mocks/any_instance/message_chains.rb#L51-L52 In that case, @chains_by_method_name contains all expected or allowed method_name in its keys, but one of the chains value is an empty array.

This seems related to https://github.com/rspec/rspec-rails/issues/1987, but related to rspec-mocks code-wise.

pirj commented 1 year ago

Would you like to send a PR to fix this, @AntoineBecquet ?

AntoineBecquet commented 1 year ago

Well, maybe if I have more time to investigate in the future, but for now I don't really know how these @chains_by_method_name are managed.

JonRowe commented 1 year ago

Thanks for the reproduction, I've fixed this in #1501, I think its just a bad assumption that there'd always be an expectation, but for some reason there isn't, this isn't an invalid state for message chains because if you remove a stub it does end up as an empty list and this is the only method that doesnt handle that eventuality.