rspec-expectations
rspec-expectations copied to clipboard
`NoMethodError` when `receive` and `have_receive` are aliased with `alias_matcher`. Conflicts with ActiveSupport's `Object#with`
Subject of the issue
Rails 7.1 added Object#with which conflicts with receive and have_received that define a #with method (chain :with) when used via an alias_matcher.
Related issue: https://github.com/rspec/rspec-expectations/issues/1437 (solved)
Your environment
- Ruby version: 3.2.2
- rspec-expectations version: 3.13.0
- Rails version: 7.1.3.2
Steps to reproduce
require "rails_helper"
RSpec::Matchers.alias_matcher :have_received_alias, :have_received
RSpec::Matchers.alias_matcher :receive_alias, :receive
RSpec.describe "Foo" do
it do
expect(1).to have_received_alias(:foo_bar).with(foo: :bar)
end
it do
expect(1).to receive_alias(:foo_bar).with(foo: :bar)
end
end
Expected behavior
Neither assert should raise an NoMethodError
Actual behavior
Arguments passed for with method are treated as Object methods.
Failures:
1) Foo
Failure/Error: expect(1).to receive_alias(:foo_bar).with(foo: :bar)
NoMethodError:
undefined method `foo' for #<RSpec::Mocks::Matchers::Receive:0x00000001107d1868>
# ./spec/dummy_spec.rb:14:in `block (2 levels) in <top (required)>'
# ./spec/rails_helper.rb:233:in `block (2 levels) in <top (required)>'
# ./spec/spec_helper.rb:147:in `block (2 levels) in <top (required)>'
2) Foo
Failure/Error: expect(1).to have_received_alias(:foo_bar).with(foo: :bar)
NoMethodError:
undefined method `foo' for #<RSpec::Mocks::Matchers::HaveReceived:0x0000000128478b68 @method_name=:foo_bar, @block=nil, @constraints=[], @subject=nil>
# ./spec/dummy_spec.rb:10:in `block (2 levels) in <top (required)>'
# ./spec/rails_helper.rb:233:in `block (2 levels) in <top (required)>'
# ./spec/spec_helper.rb:147:in `block (2 levels) in <top (required)>'
Finished in 2.84 seconds (files took 10.64 seconds to load)
2 examples, 2 failures
I’m away from my computer. Wondering if ‘receive(:foo_bar)’ and ‘receive_alias(:foo_bar)’ would return instances of the same class? Why would there be such a difference?
It seems that something is wrong with RSpec::Matchers::AliasedMatcher matcher. ActiveSupport seems to override HaveReceived methods.
I'm pasting some output from debugging session what I found, it might be helpful.
[1] pry(#<RSpec::ExampleGroups::Foo>)> have_received_alias(:foo_bar).class
=> RSpec::Matchers::AliasedMatcher
[2] pry(#<RSpec::ExampleGroups::Foo>)> have_received_alias(:foo_bar).class.superclass
=> RSpec::Matchers::MatcherDelegator
[3] pry(#<RSpec::ExampleGroups::Foo>)> have_received(:foo_bar).class
=> RSpec::Mocks::Matchers::HaveReceived
[4] pry(#<RSpec::ExampleGroups::Foo>)> have_received(:foo_bar).class.superclass
=> Object
...
[7] pry(#<RSpec::ExampleGroups::Foo>)> have_received_alias(:foo_bar).inspect
=> "#<RSpec::Mocks::Matchers::HaveReceived:0x0000000162438768 @method_name=:foo_bar, @block=nil, @constraints=[], @subject=nil>"
[8] pry(#<RSpec::ExampleGroups::Foo>)> have_received(:foo_bar).inspect
=> "#<RSpec::Mocks::Matchers::HaveReceived:0x00000001624348e8 @method_name=:foo_bar, @block=nil, @constraints=[], @subject=nil>"
[6] pry(#<RSpec::ExampleGroups::Foo>)> show-method have_received_alias(:foo_bar).with
From: gems/ruby/3.2.0/gems/activesupport-7.1.3.2/lib/active_support/core_ext/object/with.rb:4:
[5] pry(#<RSpec::ExampleGroups::Foo>)> show-method have_received(:foo_bar).with
From: gems/ruby/3.2.0/gems/rspec-mocks-3.13.0/lib/rspec/mocks/matchers/have_received.rb:53:
My current workaround:
class RailsAliasedMatcherPatched < RSpec::Matchers::AliasedMatcherWithOperatorSupport
def with(*, **)
base_matcher.__send__(:with, *, **)
end
end
RSpec::Matchers.alias_matcher :have_received_alias, :have_received, klass: RailsAliasedMatcherPatched
RSpec::Matchers.alias_matcher :receive_alias, :receive, klass: RailsAliasedMatcherPatched
I'm curious, what runs first in your setup, the Rails' patch to Object that
defines with, or ours BaseDelegator that relies on the Object to be
already patched.
Wondering if this can be load order-dependent.
I could not reproduce the issue with this (similar to https://github.com/rails/rails/issues/49958):
# frozen_string_literal: true
require "bundler/inline"
gemfile(true) do
source "https://rubygems.org"
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
gem "sqlite3"
gem 'rails', '~> 7.1'
gem 'rspec-rails'
end
require "rails/all"
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database:
":memory:")
require 'rspec/rails'
require 'rspec/autorun'
RSpec::Matchers.alias_matcher :receive_alias, :receive
RSpec.describe 'alias_matcher' do
it 'works' do
d = double
expect(d).to receive(:foo).with(1)
d.foo(1)
end
it 'does not' do
d = double
expect(d).to receive_alias(:foo).with(1)
d.foo(1)
end
end