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