Skip to content

Instantly share code, notes, and snippets.

@ritaly
Last active January 22, 2024 23:07
Show Gist options
  • Save ritaly/deb4eeb484078fb36e56ace324e96ca1 to your computer and use it in GitHub Desktop.
Save ritaly/deb4eeb484078fb36e56ace324e96ca1 to your computer and use it in GitHub Desktop.
Avoid allow_any_instance_of

Avoid allow_any_instance_of

  • Rails 5+
  • Rspec

I don't wanna be stuck with it again.

Ok, maybe this is trivial, but maybe that'll help someone:

Code example

ApiController:

def approve_comment
    if comment.update("admin_approval": 'approved')
      render json: comment
    else
      not_approved_method #do something
      render json: comment.errors, status: :unprocessable_entity
    end
  end

Legacy code in spec to refactor:

describe 'PATCH approve_comment' do
    let(:comment_to_approve) { create :comment, :pending }
    let(:action) { patch :approve_comment, params: { id: comment_to_approve.id } }

    ...

    context 'when approve returns false' do
      before do
        allow_any_instance_of(Comment).to receive(:update).and_return(false)
      end

      it 'returns errors' do
        action
        expect(response).to have_http_status :unprocessable_entity
      end
      
      it 'do sth more e.g. warn the admins' do
        action
        # expect something more
      end
    end
  end
  

Using allow_any_instance_of() force update to return false for all instances of Comment (so for for any comment object) therefore does it make possible to test private method not_approved_method.

Refactor

At first

 before do
   allow(comment_to_approve).to receive(:update).and_return(false)
 end

^ Of course doesn't work => different object in spec and controller.

Stub with instance_double(Model) / allow(Model) doesn't work too. Whatever I tried I receive: expected the response to have status code :unprocessable_entity (422) but it was :ok (200)

How to avoid using allow_any_instance_of?

I don't need it. The main problem was with objects. After all, even if my comment in controller has same id from db as comment_to_approve in spec, it was a different object.

Why?

Because I missed a method in Controller that searches a comment to approve:

def comment
  @comment ||= Comment.find(params[:id])
end

After find comment looked the same, but it wasn't same object.

Partial fix:

before
  allow_any_instance_of(described_class).to receive(:comment).and_return(comment_to_approve)
  allow(comment_to_approve).to receive(:update).and_return(false)
end

LOL? How is this supposed to be called 'the fix' ... I still see allow_any_instance_of!

Let me explain. Now, we stub that descriped class always exacly the same object. So comment method will not use find to return new object but with same ID.

This directs us to a good solution, which looks like:

before do
 allow(Comment).to receive(:find).and_return(comment_to_approve)
 allow(comment_to_approve).to receive(:update).and_return(false)
end

Now the whole stuff looks like that:

describe 'PATCH approve_comment' do
    let(:comment_to_approve) { create :comment, :pending }
    let(:action) { patch :approve_comment, params: { id: comment_to_approve.id } }

    ...

    context 'when approve returns false' do
      before do
          allow(Comment).to receive(:find).and_return(comment_to_approve)
          allow(comment_to_approve).to receive(:update).and_return(false)
      end

      it 'returns errors' do
        action
        expect(response).to have_http_status :unprocessable_entity
      end
      
      it 'do sth more e.g. warn the admins' do
        action
        # expect something more
      end
    end
  end
  
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment