Testing Phoenix Views

Last updated 16 November 2016

One of the things that immediately appealed to me about Phoenix coming from Rails its views. Once I started creating functions in my views that returned HTML, however, I was instantly confused about how to test them.

I was making a function in my LayoutView that rendered a link for a site’s navigation. It included some css framework boilerplate stuff and would also add the is-active class if the current path matched the link’s path. I wanted to write a test like this:

# the LayoutView
defmodule MyApp.LayoutView do
  use MyApp.Web, :view

  def nav_link(%{request_path: request_path}, text, path) do
    link text, to: path, class: nav_link_class(request_path, path)
  end

  defp nav_link_class(path, path), do: "nav-item is-active"
  defp nav_link_class(_, _), do: "nav-item"
end

# the tests
defmodule MyApp.LayoutViewTest do
  use MyApp.ConnCase, async: true

  alias MyApp.LayoutView

  test "nav_link is not active when not on that page" do
    link = LayoutView.nav_link(%{request_path: "/other_path"}, "Link text", "/path")
    refute String.match? link, ~r/is-active/
  end

  test "nav_link is active when on that page" do
    link = LayoutView.nav_link(%{request_path: "/path"}, "Link text", "/path")
    assert String.match? link, ~r/is-active/
  end
end

If you try to run these tests, though, you will be surprised. The reason is that the link function does not return a string. Instead, it will return something like:

{:safe, [60, "a", " class=\"nav-item\" href=\"/path\"", 62, "Link text", 60, 47, "a", 62]}

This is a tuple marked as :safe (meaning it has already been html escaped) with the second element being an IO list. You can read about why it is an IO list instead of a string in this interesting post by Big Nerd Ranch.

So, how should this be tested? You could try to match on the IO list, but I think it is a lot easier to reason about tests against an html string. I looked through the tests for Phoenix.HTML on github and came across the Phoenix.HTML.safe_to_string/1 function. This takes a tuple with :safe as its first element and an IO list as its second one and returns a string of html. This is exactly what we were hoping for. Use that in your tests and they will work as expected:

defmodule MyApp.LayoutViewTest do
  use MyApp.ConnCase, async: true

  alias MyApp.LayoutView
  import Phoenix.HTML, only: [safe_to_string: 1]

  test "nav_link is not active when not on that page" do
    link = LayoutView.nav_link(%{request_path: "/other_path"}, "Link text", "/path")
    |> safe_to_string()
    refute String.match? link, ~r/is-active/
  end

  test "nav_link is active when on that page" do
    link = LayoutView.nav_link(%{request_path: "/path"}, "Link text", "/path")
    |> safe_to_string()
    assert String.match? link, ~r/is-active/
  end
end

I hope this helps you write tests for your views in Phoenix apps. I can tell you from experience that if you let too much of your view code go untested (or let a bunch of logic sneak into your templates) you will be scared to make sweeping changes to your view-related code — so get testing!