Testing Custom Form Helpers in Phoenix

I found this great article by Jose Valim where he talks about building custom form helpers in Phoenix. I consider these kind of helpers to be a great place to add unit tests. The reason for this is that they are often used in many places and lots of edge cases/customizations may come up. As we progress on building an application, we risk breaking old functionality when we introduce new one. Unit tests will let us quickly check that everything is working as expected.

This article shows how to write some simple unit tests for functions that work with forms. This is also a good way to expose one of the things that I like the most about Phoenix: everything that’s part of the framework feels very composable and its easy to understand how it works. Therefore, isolating and testing every element of our application becomes quite simple.

The original code


defmodule TestingFormHelpers.InputHelpers do
  use Phoenix.HTML
  
  def input(form, field) do
    type = Phoenix.HTML.Form.input_type(form, field)

    wrapper_opts = [class: "form-group"]
    label_opts = [class: "control-label"]
    input_opts = [class: "form-control"]

    content_tag :div, wrapper_opts do
      label = label(form, field, humanize(field), label_opts)
      input = 
        apply(
          Phoenix.HTML.Form, 
          type, 
          [form, field, input_opts]
      )
      error = 
        TestingFormHelpersWeb.ErrorHelpers.error_tag(
          form, 
          field
        ) || ""

      [label, input, error]
    end
  end
end

We will be testing the function from the article that generates the same markup as a Phoenix generator:

When called inside a form, which is passesd as the first argument of the function, this helper turns this:

  
  input f, :field_name
  

into this:

<%= label f, :field_name, class: "control-label" %> <%= text_input f, :field_name, class: "form-control" %> <%= error_tag f, :field_name %>
  
  

Writing the tests

In order to test our function, we need a %Phoenix.HTML.Form{} struct to pass as a first argument to the function. We can create one using Phoenix.HTML.FormData.to_form/2, this transforms a struct that complies with the FormData protocol into a form struct. In Phoenix, there are two structs that comply with this protocol: Plug.Conn and Ecto.Changeset.

Let’s use a changeset as it is what we will generally be using in forms. To build one, we can use a mock schema module defined inside the same test module or in a new file, for example test/support/some_schema.ex.

  
defmodule TestingFormHelpers.SomeSchema do
  use Ecto.Schema

  schema "some_schemas" do
    field(:some_field, :string)
  end
end
  

Inside the test, we use `Ecto.Changeset.cast`, pass it to the `to_form\2` function and invoke the genereted html as a string to assert the values are being created correctly.

  
defmodule TestingFormHelpers.InputHelpersTest do
  use ExUnit.Case
  alias TestingFormHelpers.InputHelpers

  test "renders text input with phoenix-generator-style wrappers" do
    changeset =
      Ecto.Changeset.cast(
        %TestingFormHelpers.SomeSchema{}, 
        %{some_field: "Some Value"}, 
        [:some_field]
      )

    form = Phoenix.HTML.FormData.to_form(changeset, [])

    html = 
      Phoenix.HTML.safe_to_string(
        InputHelpers.input(form, :some_field)
      )

    assert(html =~ "<div class=\"form-group\"")
    assert(html =~ "<label class=\"control-label\"")
    assert(html =~ "<input class=\"form-control\"")
    assert(html =~ "type=\"text\"")
    assert(html =~ "name=\"some_schema[some_field]\"")
    assert(html =~ "value=\"Some Value\"")
  end
end
  

We can also test that when the changeset has errors, the correct error message is shown. Keep in mind that in order for the form object to show the errors, the changeset we are transforming into a form struct needs to have an action applied to it. That is why we are using Ecto.Changeset.apply_action/2.

  
test "renders errors correctly" do
  {:error, changeset} =
    Ecto.Changeset.cast(
      %TestingFormHelpers.SomeSchema{}, 
      %{some_field: "Some Value"}, 
      [:some_field]
    )
    |> Ecto.Changeset.validate_length(:some_field, max: 3)
    |> Ecto.Changeset.apply_action(:insert)

  form = Phoenix.HTML.FormData.to_form(changeset, [])

  html = 
    Phoenix.HTML.safe_to_string(
      InputHelpers.input(form, :some_field)
    )

  assert(html =~ "should be at most 3 character(s)")
end