I'm trying to mock a class, so that I can expect it is instantiated and that a certain method is then called.
I tried:
expect(MyPolicy).
to receive(:new).
and_wrap_original do |method, *args|
expect(method.call(*args)).to receive(:show?).and_call_original
end
But all I'm getting is:
undefined method `show?' for #RSpec::Mocks::VerifyingMessageExpectation:0x0055e9ffd0b530
I've tried providing a block and calling the original methods first (both :new and :show?, which I had to bind first), but the error is always the same.
I know about expect_any_instance_of
, but it's considered code-smell, so I'm trying to find another way to do it properly.
Context: I have pundit policies and I want to check whether or not it has been called
I also tried, with the same error:
ctor = policy_class.method(:new)
expect(policy_class).
to receive(:new).
with(user, record) do
expect(ctor.call(user, record)).to receive(query).and_call_original
end
You broke MyPolicy.new
.
Your wrapper for new
does not return a new MyPolicy object. It returns the result of expect(method.call(*args)).to receive(:show?).and_call_original
which is a MessageExpectation.
Instead, you can ensure the new object is returned with tap
.
# This is an allow. It's not a test, it's scaffolding for the test.
allow(MyPolicy).to receive(:new)
.and_wrap_original do |method, *args|
method.call(*args).tap do |obj|
expect(obj).to receive(:show?).and_call_original
end
end
Or do it the old fashioned way.
allow(MyPolicy).to receive(:new)
.and_wrap_original do |method, *args|
obj = method.call(*args)
expect(obj).to receive(:show?).and_call_original
obj
end
It is often simpler to separate the two steps. Mock MyPolicy.new to return a particular object and then expect the call to show? on that object.
let(:policy) do
# This calls the real MyPolicy.new because policy is referenced
# when setting up the MyPolicy.new mock.
MyPolicy.new
end
before do
allow(MyPolicy).to receive(:new).and_return(policy)
end
it 'shows' do
expect(policy).to receive(:show?).and_call_original
MyPolicy.new.show?
end
This does mean MyPolicy.new always returns the same object. That's an advantage for testing, but might break something. This is more flexible since it separates the scaffolding from what's being tested. The scaffolding can be reused.
RSpec.describe SomeClass do
let(:policy) {
MyPolicy.new
}
let(:thing) {
described_class.new
}
shared_context 'mocked MyPolicy.new' do
before do
allow(MyPolicy).to receive(:new).and_return(policy)
end
end
describe '#some_method' do
include_context 'mocked new'
it 'shows a policy' do
expect(policy).to receive(:show?).and_call_original
thing.some_method
end
end
describe '#other_method' do
include_context 'mocked MyPolicy.new'
it 'checks its policy' do
expect(policy).to receive(:check)
thing.other_method
end
end
end
Finally, inaccessible constructor calls are a headache both for testing, and they're inflexible. It's a default which cannot be overridden.
class SomeClass
def some_method
MyPolicy.new.show?
end
end
Turn it into an accessor with a default.
class SomeClass
attr_writer :policy
def policy
@policy ||= MyPolicy.new
end
def some_method
policy.show?
end
end
Now it can be accessed in the test or anywhere else.
RSpec.describe SomeClass do
let(:thing) {
described_class.new
}
describe '#some_method' do
it 'shows its policy' do
expect(thing.policy).to receive(:show?).and_call_original
thing.some_method
end
end
end
This is the most robust option.