Expect mock result to receive method

1.5k Views Asked by At

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
1

There are 1 best solutions below

0
Schwern On BEST ANSWER

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.