Search code examples
rubyrspecpunditrspec-mocks

Expect mock result to receive method


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

Solution

  • 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.