V2.1: Testing


Views in Hanami are designed to encourage easier testing of your views, with each aspect of views designed to support direct unit testing. This means you can test your views at whatever level of granularity makes sense for you.

Testing views

To test a view directly, initialize it, passing in any dependencies it requires. If the view’s dependencies are provided via the deps mixin, then a simple .new should be sufficient. Provide test doubles for certain dependencies if you want to simulate certain conditions.

Then, call the view with the expected arguments, and make your test expectations against its output.

Given this view:

# app/views/profile/show.rb

module Profile
  class Show < Main::View
    include Deps[users_repo: "repos.user_repo"]

    expose :current_user

    expose :user do |id:|
      user_repo.by_id(id)
    end
  end
end
<%# app/templates/views/profile/show.html.erb %>

<% if current_user.id == user.id %>
  <p class="text-base">This is your profile. You can edit your data.</p>
<% else %>
  <p class="text-base">This is the profile of <%= user.name %>. You can admire it.</p>
<% end %>

A test could look like this:

# spec/unit/views/profile/show_spec.rb

RSpec.describe Views::Profile::Show do
  subject(:view) { described_class.new(users_repo:) }

  let(:user_repo) { double(:user_repo) }
  let(:current_user) { double(:user, name: "Amy", id: 1) }

  context "user inspects their profile" do
    it "renders their profile" do
      allow(user_repo).to receive(:by_id).with(1).and_return(current_user)

      output = view.call(current_user:, id: 1).to_s

      expect(output).to include("This is your profile. You can edit your data.")
    end
  end

  context "user inspects someone else's profile" do
    let(:other_user) { double(:user, name: "Lena", id: 2)}

    it "renders the other user's profile" do
      allow(users_repo).to receive(:by_id).with(2).and_return(other_user)

      output = view.call(current_user:, id: 2).to_s

      expect(output).to include("This is the profile of Lena. You can admire it.")
    end
  end
end

Views return from `#call` an instance of `Hanami::View::Rendered`. To get a plain string, use `#to_s`.

Testing exposures

To test a view’s exposures directly, you can access them on the Hanami::View::Rendered object returned after calling your view.

describe "exposures" do
  subject(:rendered) { view.call(current_user: current_user, id: 1) }

  let(:user) { double(:user, name: "Amy", id: 1) }

  before do
    allow(users_repo).to receive(:by_id).with(1).and_return(user)
  end

  it "exposes current_user" do
    expect(rendered[:current_user].name).to eq(current_user.name)
  end

  it "exposes user" do
    expect(rendered[:user].id).to eq(user.id)
  end
end

Testing parts

To test part behavior, initialize a part and make your expecations against its methods.

module MyApp
  module Views
    module Parts
      class User < MyApp::Views::Part
        def display_name
          "#{name} (#{email})"
        end
      end
    end
  end
end

RSpec.describe(MyApp::Views::Parts::User) do
  subject(:part) { described_class.new(value: user) }
  let(:user) { double(:user, name: "Amy", email: "amy@example.com"}

  describe "#display_name" do
    it "includes the name and email" do
      expect(part.display_name).to eq "Amy (amy@example.com)"
    end
  end
end

You can also use the same approach to test part behavior that requires helpers, renders partials, or accesses the context.

module MyApp
  module Views
    module Parts
      class User < MyApp::Views::Part
        def display_name
          "#{name} (#{email})"
        end

        # Using a helper
        def title_tag
          helpers.tag.h1(display_name)
        end

        # Rendering a partial at templates/users/_info_card.html
        def info_card
          render("users/info_card")
        end
      end
    end
  end
end

RSpec.describe(MyApp::Views::Parts::User) do
  subject(:part) { described_class.new(value: user) }
  let(:user) { double(:user, name: "Amy", email: "amy@example.com"}

  describe "#title_tag" do
    it "includes the name and email in a h1 tag" do
      expect(part.title_tag).to eq "<h1>Amy (amy@example.com)</h1>"
    end
  end

  describe "#info_card" do
    it "renders the info card" do
      expect(part.info_card).to start_with %(<div class="user-info-card">)
    end
  end
end