rspec / rspec-mocks

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

Cannot proxy frozen objects, rspec-mocks relies on proxies for method stubbing and expectations. #1446

Closed Loschcode closed 1 year ago

Loschcode commented 2 years ago

Subject of the issue

I moved from rspec-mocks 3.9.0 to 3.10.0 recently and it breaks several tests that were working before. Typically, it'll throw this message Cannot proxy frozen objects, rspec-mocks relies on proxies for method stubbing and expectations.

Your environment

Steps to reproduce

After investigating, it occurs systematically when I use allow_any_instance_of against a model in Rails. I couldn't find anyone talking about it precisely anywhere, which's odd.

For example, if I do

allow_any_instance_of(Identity).to receive(:destroy_hook_service)

And then call my subject it'll crash. I know we should avoid doing that and prefer stubbing a precise record, but from time to time you really want to cover all the instances of the classes. In this example, I can't actually target a specific identity, it's checking global destruction of some sort.

Expected behavior

It stubs/mocks some methods

Actual behavior

It crashes.

JonRowe commented 2 years ago

After investigating, it occurs systematically when I use allow_any_instance_of against a model in Rails. I couldn't find anyone talking about it precisely anywhere, which's odd.

1356 and #1357

This previously raised a type error when attempting to proxy onto a frozen object, and now raises this argument error explicitly. It's possible something in Rails is errorenously saying its frozen due to its own proxying and triggers this without triggering the original error, but we'd need more details to reproduce. Frozen objects are definietly not modifiable by our mocks.

aamine commented 2 years ago

I have identified the root cause of this issue. Following code reproduces the problem:

class Tmp
  def result
    'true result'
  end
end

describe 'Tmp' do
  before do
    allow_any_instance_of(Tmp).to receive(:result).and_return('mocked result')
  end

  it 'does not raise any exception' do
    tmp = Tmp.new
    expect(tmp.result).to eq('mocked result')   ### <--- This line is important
    tmp.freeze
    expect(tmp.result).to eq('mocked result')
  end
end

If we have ### <--- This line is important line, second expectation does NOT raise any exception on rspec-mock 3.9. If we remove the important line, we get FrozenError on also 3.9.

It seems that background story is like this: rspec-mock prepends its own anonymous module (is a kind of RSpecPrependedModule) on to the subject object's singleton class, and when the anonymous module is already prepended, rspec-mock reuses the anonymous module to add a stub method. Therefore, if we had had any expectation before the subject object is frozen, we can avoid FrozenError even after the subject object is frozen.

With rspec-mock 3.10, we always get error on the frozen subject object due to explicit frozen check.

aamine commented 2 years ago

Ahhh sorry, my explanation above is completely wrong... this is just because overridden tmp.result method remains. I'm investigating again...

simonxmh commented 1 year ago

I'm getting this same issue. I'm wondering if you ever reached a conclusion?

JonRowe commented 1 year ago

I'm going to call this closed by #1401, thanks to @keegangroth we now rescue the error to reraise with the warning, let me know if that doesn't solve your problems.