Alex's Slip-box

These are my org-mode notes in sort of Zettelkasten style

view_component testing

I’m getting acquainted with view_component, which is a Ruby library for building view components (surprise) in Ruby on Rails. It seems to be what the cool kids are into these days, as an alternative to standard, out-of-the-box RoR views and view partials. Server based MVC is still the name of the game, here, but with better encapsulation. A view component is a plain ruby object that is initialized with whatever arguments, and defines what is exposed to a view template (erb, haml, slim). It’s nice because it brings OOP to the view layer in RoR.

There’s something to learn about testing these things, for which the documentation is a great starting place. But pretty quickly I found myself having to figure some things out, which, I acknowledge might be because I’m not component-ing the “right” way.

For example, I have a component that depends on another component. I imagine this a common scenario, as it is with building frontends in VueJS.

# /app/views/components/message_component.rb
class MessageComponent < ApplicationViewComponent
  attr_reader :message

  def initialize(message:)
    @message = message

  def id

With this template:

-# /app/views/message_component/message_component.html.haml
%div{ id: id }
  = render [current_user, message], attribute: :content) do |component|
    = component.with_field_slot do
      = message.content
  = link_to t('message.edit'), edit_user_message_path(current_user, message)

I want to test the MessageComponent in isolation. I want to mock the InlineEditComponent. What makes it even more interesting if the fact that the InlineEditComponent has a slot which is populated by the MessageComponent. It took some trial and error, but I’ve so far settled on this:

RSpec.describe MessageComponent, type: :component do
  subject { page }
  let(:message) { build_stubbed(:message, :with_user, content: message_content) }
  let(:user) { message.user }
  let(:message_content) { Faker::Lorem.sentence }
  let(:component) { }
  let(:inline_edit_component) do do
      renders_one :field_slot
      haml_template <<~HAML
        = field_slot

  before do
    stub_const('InlineEditComponent', inline_edit_component)
    allow(InlineEditComponent).to receive(:new).and_call_original
    with_current_user(user) { render_inline(component) }

  it { have_link 'Edit Message' }

  it 'instantiates the InlineEditComponent with the proper args' do
    expect(InlineEditComponent).to have_received(:new).with(model: [user, message], attribute: :content)

  it 'renders the InlineEditComponent' do
    expect(page).to have_text('InlineEditComponent')

  it { have_text message_content }

It defines a mock class of the InlineEditComponent, which provides the bare minimum to function as a container for a slot and a template. With this, I can test that the rendered output does include the InlineEditComponent along with the field_slot through which the message.content is rendered. I’m uneasy about mocking the slot this way, though. For one thing, this test will still pass even if I change the name of the slot on the InlineEditComponent. It would be nice to get a kind of verifying double behavior on a slot. Anyway, that’s what feature/system tests are for, right? This is good enough for now.

A couple other things worth pointing out:

  • I could not use an anonymous class for the mock (eg, It would break due to the view_component expecting the component class should have a name.
  • The with_current_user thing in the before block comes from dry-effects. In short, the implementation here provide Devise’s current_user to the component keeping one from have the pass current_user to every components initialize method. There’s a detailed description of this technique is the useful Evil Martian’s 2-parter on view_components.

Search Results