Getting Rails on Track - Part 2: Views

In this multi-series blog post, we're going hands-on through the steps that can help transform the design of a default Rails application into one with clearer responsibilities and that is easier to test.

Have a look at the other parts, too!

You can follow along with a repository available on GitHub that contains all the code discussed here.

Ruby's .erb templates are the default for Rails' views. Technically they are Ruby code mixed with HTML. While this provides a tremendous amount of flexibility, it can also lead to a lot of logic in the views.

The downside to this is the more logic we put into the views, the harder it becomes to test that logic. There are tools like Capybara or Selenium to help us with that, though. And there are times where this testing approach is worthwhile. But it shouldn't be the first choice to do that, as these tests tend to be slow and brittle. Frontend testing often involves rendering the UI, which significantly increases the duration of the test suite. Additionally, tests that rely on class names or even website copy are more likely to fail for the wrong reasons once those change.

Hence, from a general testing point of view, we should try not to test our application through the UI. Sometimes this is also referred to as “submarine testing”—the testing starts just after the UI layer, i.e. one layer below the surface of the application.

This forces us to keep our views as dumb as possible. The more logic we put in there, the more logic we can't test with this approach.

One way to achieve that is to introduce presenters that the views can use. Ideally a view only calls methods on that presenter with as little conditional logic as possible. Having said that, it is not always easy to end up there—and also not always needed. For example it can be okay having something like the following code in a view:

<% if presenter.logged_in?(current_user) %>
  <a href="/logout">Log out</a>
<% else %>
  <a href="/login">Log in</a>
<% end %>

As long as logged_in? of the presenter has the appropriate unit tests. It's oftentimes okay having this kind of conditional logic in a view because it is still testable in an easy enough manner, even though it might be a manual test.

Presenting Movies

Continuing with the movie example, let's have a look at how a movie gets displayed.

RSpec.describe MoviesController
  it "renders the show template" do
    movie = Movie.create(title: "the title")
    get :show, id: movie.id

    expect(response).to render_template("show")
  end
end

Making that test pass is relatively straightforward. The controller looks up a movie from the database and provides it for the view in the form of an instance variable.

class MovieController < ApplicationController
  def show
    @movie = Movie.find(params[:id])
  end
end

The view then formats the movie's data.

<h1><%= @movie.title.titleize %></h1>
<h2><%= @movie.release_date.strftime("%e %B %Y") %></h2>

<p><%= @movie.description %></p>

What—if anything—can be improved here?

The smell here is the logic in the view. To be precise, the calls to titleize and strftime contain domain logic, and should be tested in isolation. But since this is directly written in the view, it is not as straightforward to test as it could be.

Adding a Movie Presenter

The logic for formatting a movie's data can be wrapped in a presenter. Let's start with a test for its title.

RSpec.describe MoviePresenter do
  it "titleizes the title" do
    movie = Movie.new(title: "the movie title")
    presenter = MoviePresenter.new(movie)

    expect(presenter.title).to eq("The Movie Title")
  end
end

To make that test pass, we move the code we already have in the view to the new MoviePresenter.

class MoviePresenter
  def initialize(movie)
    @movie = movie
  end

  def title
    movie.title.titleize
  end

  private

  attr_reader :movie
end

In a similar fashion, we can add a test for the release_date.

it "formats the release date" do
  movie = Movie.new(release_date: Date.parse("1984-06-08"))
  presenter = MoviePresenter.new(movie)

  expect(presenter.release_date).to eq("8 June 1984")
end

Making this test pass is also as straightforward as moving the code from the view to the presenter. As we do this, we can even fix a bug while we're at it, as the original format string "%e" adds a space in front of the day. What we actually want is just the day of the month without a padded zero or space (i.e. "%-d").

def release_date
  movie.release_date.strftime("%-d %B %Y")
end

The presenter should be able to present all the data of a movie, so let's add a last test for the description.

it "presents the description" do
  movie = Movie.new(description: "the description")
  presenter = MoviePresenter.new(movie)

  expect(presenter.description).to eq(movie.description)
end

We don't have any new behaviour around the description for a movie, which makes the implementation a simple delegation.

def description
  movie.description
end

An alternative implementation for a delegation could make use of Ruby's Forwardable module. The example doesn't use it because it was only one method we need to delegate here. Forwardable can be a bigger benefit when there are many methods to delegate.

The full code of the MoviePresenter and its tests is available on GitHub.

Putting It Back Together

Now that we have a presenter we can use it in the controller. For that, we first add a test to reflect that we actually do get a presenter.

it "wraps a movie in a presenter" do
  movie = Movie.create(title: "the title")
  get :show, id: movie.id

  expect(assigns(:movie)).to be_a(MoviePresenter)
end

To make that test pass, we update the controller's code as follows.

def show
  @movie = MoviePresenter.new(Movie.find(params[:id]))
end

And use the presenter in the view.

<h1><%= @movie.title %></h1>
<h2><%= @movie.release_date %></h2>

<p><%= @movie.description %></p>

Now the view doesn't know how to format a release date or a title anymore. It's simply viewing whatever it gets.

Presenting a List of Movies

Let's have a look at the movie list. The following code shows the controller test as well as the view code.

RSpec.describe MovieController
  it "renders the overview" do
    get :index

    expect(response).to render_template("index")
  end
end
<ul>
  <% Movie.each do |movie| %>
    <li><%= movie.title.titleize %></li>
  <% end %>
</ul>

While this absolutely does work, one design smell here is that the view reaches down to a model class, which ties the view directly to the database. One way to improve that is by introducing a variable the view can use to iterate over the available movies.

it "has a list of movies" do
  get :index

  expect(assigns(:movies)).to be_an(Array)
end
class MoviesController < ApplicationController
  def index
    @movies = Movie.all.to_a
  end
end
<ul>
  <% @movies.each do |movie| %>
    <li><%= movie.title.titleize %></li>
  <% end %>
</ul>

That is a small improvement over the original version, but it still bears some problems. Now that we introduced an abstraction in form of an instance variable, what happens if we rename @movies to @all_movies? The tests for the controller verify the existence of that variable and would catch that. Not an ideal unit test, because it is not testing any real behaviour, but still a small safety net. It does not tell us if the view actually uses the correct variable, though. This way we would end up with a green test suite but a broken application. Only by manually testing the view again could we catch that bug.

Testing Views?

The issue just presented is a common one. It's not a problem with Rails per se, but this happens often when we have a UI that we want to test. There are many ways to solve it, and we're going to discuss two possible options.

We've just seen one way to solve that: verifying the existence of an instance variable. As mentioned, this approach does not completely guard us against the issue of a renamed variable (or even a typo in the view's code). Tests like that only verify that something the view could use is available. They don't verify that the view actually uses it.

Another way is to actually build the view in the tests. With that, we can verify that the template can be created without errors. Rails (more specifically rspec-rails) has a helper method for that called render_views. Calling that method first will cause the controller under test to try building the final view that would be sent to the client.

With that we can write a test that will fail every time the view can not be created, e.g. because a used variable is not initialised.

RSpec.describe MoviesController
  render_views

  it "renders the overview" do
    get :index

    expect(response).to render_template("index")
  end
end

But depending on how big the views become, there can be a downside to this approach: building the actual view can slow down our tests. But it's worth mentioning that the increased reliability of the test suite can be worth the time it takes to run it.

Both ways are valid options, and both ways should be considered for the specific use case at hand. A good middle ground could be to have a few tests that use render_views to make sure everything is wired up correctly, and all other tests exercise the controller without rendering the views.

It's a trade-off between test run time and reliability of the test suite. This trade-off should be discussed and decided on a case by case basis.

Christoph Gockel, Software Craftsman

Christoph Gockel is a Software Craftsman who enjoys writing clean code almost as much as watching Ghostbusters.

Interested in 8th Light's services? Let's talk.

Learn more about our Ruby services

Contact Us