rspec/rspec-rails

"expect to receive with" throws error when switching to Rails 7.1

fsuchan opened this issue · 26 comments

What Ruby, Rails and RSpec versions are you using?

Ruby version: 3.2.1
Rails version: 7.1.1
RSpec version: 3.12 (rspec-rails 6.0.3)

Observed behaviour

Example spec expect(Worker).to receive(:perform_async).with(param1, param2)

returns ArgumentError: wrong number of arguments (given 2, expected 0)

Switching to deprecated Worker.should_receive(:perform_async).with(param1, param2) passes.

Expected behaviour

Passes with "expect to receive with" syntax.

Can you provide an example app?

pirj commented

Can you reproduce it with a regular object?
What is the backtrace? Can you set a breakpoint and see what method is called with two arguments, which doesn’t really accept any?
Is that a Sidekiq worker Worker?

@pirj yes, that's a Sidekiq worker 👍

Full example that worked on Rails 7.0 but not on 7.1:

it 'sends an email' do
  user = Fabricate(:user, encrypted_password: nil)
  
  expect(EmailWorker).to receive(:perform_async).with('UserInvite::Email', user.id)
  
  Invite::Create.call(user:)
end
pirj commented

Can you please share your invite::Create.call?
Can you make a reproducible example? Check our snippets directory for examples.

Were you able to identify the method that it complains about?

I have a possibly relevant issue with the same gem/ruby versions. It does not fail with ArgumentError.

However, it carries the stub/expectation through different examples and fails with a multiple calls error:

     Failure/Error: expect(ListSubscriptionMailer).to receive(:email_confirmation).and_return(mock_mail)

       (ListSubscriptionMailer (class)).email_confirmation(*(any args))
           expected: 1 time with any arguments
           received: 2 times with any arguments
pirj commented

@StanBright i suggest you to yse ‘and_wrap_original’ and to figure out what makes the second call.

The second call is coming from a different spec case. The specs are run randomly, and it fails randomly - i.e. if the spec with the mocked expectation is run before the other case, there's no error.

The same mocking works fine on Rails 7.0 projects. Seemingly, there's something odd with Rspec and Rails 7.1.

vrinek commented

Regarding the initial issue, I am getting the same for every with in my specs after upgrading to Rails 7.1:

     ArgumentError:
       wrong number of arguments (given 2, expected 0)
     # /usr/local/bundle/gems/activesupport-7.1.1/lib/active_support/core_ext/object/with.rb:24:in `with'
     # ./spec/lib/mail_configurator_spec.rb:43:in `block (4 levels) in <top (required)>'
     # /usr/local/bundle/gems/webmock-3.14.0/lib/webmock/rspec.rb:37:in `block (2 levels) in <top (required)>'

According to ActiveSupport's changelog, Object#with was added with 7.1.0.beta1:

  • Add Object#with to set and restore public attributes around a block

    client.timeout # => 5
    client.with(timeout: 1) do
      client.timeout # => 1
    end
    client.timeout # => 5

Relevant commit: rails/rails@1884323

pirj commented

Thanks for digging that, @vrinek
The problem is here then
‘’’
next if own_methods.include?(method)
‘’’
https://github.com/rspec/rspec-mocks/blob/868bf98d2aae5f0db15441a41b86b5f9346a12dd/lib/rspec/mocks/matchers/receive.rb#L60

We have a fix for this already, but it doesn’t work somehow rspec/rspec-mocks@a2af97d

Cc @byroot

byroot commented

If someone share a reproduction script or repo, I'll happily debug this. But right now the repro steps are too blurry for me to investigate.

On Rails 7.1 any test that uses the expect(x).to receive(:foo).with('bar') pattern will break because Rails introduced their own Object#withpatch in ActiveSupport that interferes with the RSpec with method: https://github.com/rails/rails/blob/7-1-stable/activesupport/lib/active_support/core_ext/object/with.rb

See comment below.

Ah this was actually fixed for us by ensuring the rspec-mocks dependency was updated to 3.12.5, rather than only checking that rspec/rspec-rails was up-to-date.

I notice the original issue does not include the rspec-mocks version so it's possible that it's the same issue of not noticing the child dependency versions.

There was a patch kindly provided by byroot in rspec-mocks 3.12.5 (current is 3.12.6) @fsuchan what version of rspec-mocks are you running?

@JonRowe can confirm, 3.12.6 solves the issue, we were still on 3.12.3 👍

I'm running into this exact issue, except it's not rspec-mocks, but instead it's happening with a define_negated_matcher, and as an example ,enqueue_job:

 RSpec::Matchers.define_negated_matcher(:not_enqueue_job, :enqueue_job)

  it 'does not enqueue a job' do
    expect { 'Foo'.downcase }.to not_enqueue_job.with('Bar')
  end

I think we have a few custom matchers that run into this issue too.

I'm gonna open an issue on the rails repo to see what they say, but I wanted to bring this up here as well!

Cheers.

byroot commented

@vimalloc I suspect not_enqueue_job is in something like rspec-rails, better open your issue there. You can tag me as well, I'll have a look if I can.

@byroot thank you! I have an issue with a lot more details here, if you want to look at it! rails/rails#49958

byroot commented

I provided a workaround on the Rails issue, and started working on a fix at rspec/rspec-expectations#1434.

If there is a maintainer that could approve CI so I can see what need to be fixed for very old rubies I can't run on my machine, that would be much appreciated.