Mocking all Rails routes in a view_context

The Rails controller and views provide a view_context (usually an ActionView::Base object) that provides context for generating the views.

A common pattern is to wrap model instances in a Presenter class, in which case the view_context is usually also passed as an argument so the Presenter can call view methods (e.g. I8n.t(), Rails path helpers, etc...) as needed.

In my RSpec tests I use a mock to test the view_context behavior within the Presenter. For the path helpers specifically, I have to mock each path individually:

view_context = ActionView::Base.new
user = UserPresenter.new(FactoryBot.create(:user), view: view_context)

allow(view_context).to receive(:some_custom_path) do |opts|
  some_custom_path(opts)
end

Is there an easy way to programmatically mock all paths at once?

I suppose I could loop through the list of paths (not sure how to do that) and mock each one by one, but it feels like not the right approach.

Thanks!

EDIT: Actually the above snippet isn't even correct. It throws an error because the view_context (ActionView::Base) doesn't even implement :some_custom_path in the first place. I'm guessing it's a protection measure against stubbing something that doesn't exist.

1 answer

  • answered 2019-03-14 06:53 Fito von Zastrow

    Why do you want to mock all paths?

    I'm assuming you are interested in actually mocking these calls and not just stub them. See the difference here.

    Different presenters will probably call different path methods on their view_context. I recommend that you explicitly mock only the paths you are expecting to be called within the presenter you are testing.

    You don't need to mock all paths because they are not all going to be called every time.

    I would write your test as follows:

    describe UserPresenter do
      subject(:user_presenter) { described_class.new(user, view: view_context)
    
      let(:user) { FactoryBot.create(:user) }
      let(:view_context) { instance_double(ActionView::Base) }
      let(:some_custom_path) { 'some/custom/path' }
    
      before do
        allow(view_context).to receive(:some_custom_path).and_return(some_custom_path)
      end
    
      it 'does something'
    end
    

    About the error you are seeing, yes, instance_double will protect you against stubbing a method that is not implemented on the receiver.

    I do not recommend you do this, but if all you are looking for is a view object that will silently swallow calls to path methods then you can create a fake view like this:

    class FakeView
      private
    
      def view_methods
        ActionView::Base.instance_methods - Object.instance_methods
      end
    
      def method_missing(meth, *params, &block)
        view_methods.include?(meth) ? nil : super
      end
    end
    

    and then use it in your tests like:

    describe UserPresenter do
      subject(:user_presenter) { described_class.new(user, view: view_context)
    
      let(:user) { FactoryBot.create(:user) }
      let(:view_context) { FakeView.new }
    
      it 'does something'
    end