I recently created a Presenter object to wrap around an ActiveStorage::Blob that I wanted to decorate with some presentation methods for things like a human file size and content type.

The initialisation signature for my presenter looks like this:

class MyPresenter < SimpleDelegator
  def initialize(obj, view_context)
    super(obj)
    @view_context = view_context
  end

  def human_file_size
    @view_context.number_to_human_size(byte_size, precision: 1)
  end

  # more presentation methods...
end

I went to test this, and got a bit stuck! I needed to provide a view context to this class that I could call view helpers on. I could pass in a double, but would then have to stub out every single view helper that the presenter used. This would mean my tests would be testing my stubs, not the actual behaviour. I could use a double.as_null_object, but that assumes that all the helpers are OK to return nil - that’s an extremely uncommon thing for a view helper to do!

I stopped and thought for a minute - I know that there are several types of RSpec tests where you can access a view context - particularly controller and view specs. I decided to begin my investigation with view specs, since this was most closely aligned with what I was trying to test.

I started with the source of rspec-rails, particularly lib/rspec/rails/view_rendering.rb. It sort of looked relevant, but it looked like it was using a lot of methods coming from elsewhere - things like controller, and lookup_context.

Next, I looked to see where this module was used, and there I found lib/rspec/rails/example/view_example_group.rb. This looked even more relevant - I could see things like controllers, and helpers being set up. Great! All of these were using a method named view, though, and while the method documentation for this looked relevant - “The instance of ActionView::Base that is used to render the template.” - the actual method body just returned the value of _view - and this wasn’t instantiated inside this module.

Looking at the top of this module, I could see an include from Rails itself - include ActionView::TestCase::Behavior. This was a real giveaway, because all of the other top-level includes were for other modules from within rspec-rails.

I switched over to the Rails repository, and headed into lib/action_view to find this class, and I found it at actionview/lib/action_view/test_case.rb.

Within this class, I found the view method. This method was aliased to _view, which was what RSpec was using.

In setup_with_controller, I could see that the @controller variable was set to an instance of ActionView::TestCase::TestController. Neat! This means that the set up of a view context is entirely contained to this class, and I can do exactly what the view spec module is doing - include ActionView::TestCase::Behaviour, and then access the view method in my spec to use the view context.

As an example:

RSpec.describe MyPresenter, type: :presenter do
  include ActionView::TestCase::Behavior

  describe "#file_size_human" do
    it "returns the expected value" do
      format = ActiveStorage::Blob.new(byte_size: 1500)
      expect(described_class.new(format, view).file_size_human).to eq "1.5 KB"
    end
  end
end