Hashrocket.com / blog

Large maine 2

Integration Testing Phoenix With Wallaby

posted on and written by in

Image 100x100 jake worth

Let's write an integration test for Phoenix using Wallaby.

Integration tests are used for behavior description and feature delivery. From the test-writer's perspective, they are often seen as a capstone, or, to the outside-in school, an initial 10,000-foot description of the landscape. At Hashrocket we've been perusing the integration testing landscape for Elixir, and recently wrote a suite using Wallaby.

In this post, I'll walk through a test we wrote for a recent Phoenix project.

Overview

Two emerging tools in this space are Hound and Wallaby; they differ in many ways, enumerated in this Elixir Chat thread.

The Wallaby team describes the project as follows:

Wallaby helps you test your web applications by simulating user interactions. By default it runs each TestCase concurrently and manages browsers for you.

We chose Wallaby for our project– an ongoing Rails-to-Phoenix port of Today I Learned (available here)– because we liked the API. It's similar to Ruby's Capybara.

Setup

Here are the basic steps we took to create our first Wallaby test.

Wallaby concurrently powers multiple PhantomJS headless browsers. To leverage that feature we'll need PhantomJS:

$ npm install -g phantomjs

Next, add Wallaby to your Phoenix dependencies:

# mix.exs

def deps do
  [{:wallaby, "~> 0.14.0"}]
end

As always, install the dependencies with mix deps.get.

Ensure that Wallaby is properly started, using pattern matching, in your test_helper.exs:

# test/test_helper.exs

{:ok, _} = Application.ensure_all_started(:wallaby)

If you're using Ecto, enable concurrent testing by adding the Phoenix.Ecto.SQL.Sandbox plug to your endpoint (this requires Ecto v2.0.0-rc.0 or newer). Put this is at the top of endpoint.ex, before any other plugs.

# lib/tilex/endpoint.ex

if Application.get_env(:your_app, :sql_sandbox) do
  plug Phoenix.Ecto.SQL.Sandbox
end
# config/test.exs

# Make sure Phoenix is setup to serve endpoints
config :tilex, Tilex.Endpoint,
  server: true

config :tilex, :sql_sandbox, true

Use test_helper.exs for any further configuration, like so:

# test/test_helper.exs

Application.put_env(:wallaby, :base_url, "http://localhost:4001")

We also enabled a feature which saves a screenshot on every failure. This is crucial when testing with a headless browser:

# config/test.exs

config :wallaby, screenshot_on_failure: true

That's the basic setup. If you get stuck, here's the pull request where we made all these changes.

Testing

Anytime we're writing a test, we want to extract shared logic into a central place. Phoenix accomplishes this with the IntegrationCase concept. This module does a lot of things, including importing other modules, defining aliases, and assigning variables.

I don't want to dig too deeply into this file, but will include it here in its entirety so all our setup is clear. A few noteworthy points are import Tilex.TestHelpers, which will let us build custom helper functions, and the setup tags block, which I copied directly from the Wallaby docs.

# test/support/integration_case.ex

defmodule Tilex.IntegrationCase do

  use ExUnit.CaseTemplate

  using do
    quote do
      use Wallaby.DSL

      alias Tilex.Repo
      import Ecto
      import Ecto.Changeset
      import Ecto.Query

      import Tilex.Router.Helpers
      import Tilex.TestHelpers
    end
  end

  setup tags do
    :ok = Ecto.Adapters.SQL.Sandbox.checkout(Tilex.Repo)

    unless tags[:async] do
      Ecto.Adapters.SQL.Sandbox.mode(Tilex.Repo, {:shared, self()})
    end

    metadata = Phoenix.Ecto.SQL.Sandbox.metadata_for(Tilex.Repo, self())
    {:ok, session} = Wallaby.start_session(metadata: metadata)
    {:ok, session: session}
  end
end

Let's test!

We're going to assert about a user's (or a developer's, in the language of this app) ability to create a new post through a form. Forms are fantastic subjects for integration tests because there are many edge cases.

First, create a test file. Here's the structure, which defines our test module and includes our IntegrationCase module. We use async: true to configure this test case to run in parallel with other test cases.

# test/features/developer_create_post_test.exs

defmodule DeveloperCreatesPostTest do
  use Tilex.IntegrationCase, async: true
end

I start by creating an empty test to confirm my setup.

# test/features/developer_create_post_test.exs

defmodule DeveloperCreatesPostTest do
  use Tilex.IntegrationCase, async: true

  test "fills out form and submits", %{session: session} do
  end
end

Run it with mix test:

$ mix test

warning: variable session is unused
  test/features/developer_creates_post_test.exs:6

.

Finished in 1.3 seconds
1 test, 0 failures

Okay, our test runs. Also, notice that Elixir compile-time warning, variable session is unused? That's the kind of perk I love about writing in this language. We'll keep the variable, because it will prove useful.

What now? Let's fill out the form, which looks like this:

<% # web/templates/post/form.html.eex %>

<%= form_for @changeset, post_path(@conn, :create), fn f -> %>
  <dl>
    <dt>
      <%= label f, :title %>
    </dt>
    <dd>
      <%= text_input f, :title %>
    </dd>
  </dl>
  <dl>
    <dt>
      <%= label f, :body %>
    </dt>
    <dd>
      <%= textarea f, :body %>
    </dd>
    <dt>
      <%= label f, :channel_id, "Channel" %>
    </dt>
    <dd>
      <%= select f, :channel_id, @channels, prompt: "" %>
    </dd>
  </dl>
  <%= submit "Submit" %>
<% end %>

Our test will use Ecto Factory to generate a channel struct, which we'll need to select the channel on the form. All the test code that follows is implied to be inside our test block.

EctoFactory.insert(:channel, name: "phoenix")

visit(session, "/posts/new")
h1_heading = get_text(session, "main header h1")
assert h1_heading == "Create Post"

So we create a channel, use the visit/2 function to visit our new post path, and then get the text from the header and make an assertion about it.

get_text/2 is a custom helper function, available in a test_helpers.exs file that we already imported via our IntegrationCase module. It extracts a common task: getting the inner text from an HTML selector. Here's the definition:

# test/support/test_helpers.ex

defmodule Tilex.TestHelpers do

  use Wallaby.DSL

  def get_text(session, selector) do
    session |> find(selector) |> text
  end
end

Extract as many helpers as your tolerance for abstraction allows.

Okay, time to fill in the form. Here, our session variable and Elixir's pipe operator (|>) shine.

session
|> fill_in("Title", with: "Example Title")
|> fill_in("Body", with: "Example Body")
|> Actions.select("Channel", option: "phoenix")
|> click_on('Submit')

We'll alias Wallaby.DSL.Actions to Actions for convenience. Why be so verbose at all? Because select/3 is ambiguous with Ecto.Query.

Let's assert about our submission. If it worked, we should be on the post index page, which looks like this:

<% # web/templates/post/index.html.eex %>

<section id="home">
  <%= for post <- @posts do %>
    <%= render Tilex.SharedView, "post.html", conn: @conn, post: post %>
  <% end %>
</section>

And here's the post partial it references, which we'll assert about:

<% # web/templates/shared/post.html.eex %>

<article class="post">
  <section>
    <div class="post__content copy">
      <h1>
        <%= link(@post.title, to: post_path(@conn, :show, @post)) %>
      </h1>
      <%= raw Tilex.Markdown.to_html(@post.body) %>
      <footer>
        <p>
          <br/>
          <%= link(display_date(@post), to: post_path(@conn, :show, @post), class: "post__permalink") %>
        </p>
      </footer>
    </div>
    <aside>
      <ul>
        <li>
          <%= link("##{@post.channel.name}", to: channel_path(@conn, :show, @post.channel.name), class: "post__tag-link") %>
        </li>
      </ul>
    </aside>
  </section>
</article>

Okay, on to the assertions:

index_h1_heading = get_text(session, "header.site_head div h1")
post_title       = get_text(session, ".post h1")
post_body        = get_text(session, ".post .copy")
post_footer      = get_text(session, ".post aside")

assert index_h1_heading =~ ~r/Today I Learned/i
assert post_title       =~ ~r/Example Title/
assert post_body        =~ ~r/Example Body/
assert post_footer      =~ ~r/#phoenix/i

Once again we use our helper function get_text/2 to capture the text on the page. Then, we use ExUnit to assert about the copy we found.

Here it is all together:

# test/features/developer_create_post_test.exs

defmodule DeveloperCreatesPostTest do
  use Tilex.IntegrationCase, async: true

  alias Wallaby.DSL.Actions

  test "fills out form and submits", %{session: session} do

    EctoFactory.insert(:channel, name: "phoenix")

    visit(session, "/posts/new")
    h1_heading = get_text(session, "main header h1")
    assert h1_heading == "Create Post"

    session
    |> fill_in("Title", with: "Example Title")
    |> fill_in("Body", with: "Example Body")
    |> Actions.select("Channel", option: "phoenix")
    |> click_on('Submit')

    index_h1_heading = get_text(session, "header.site_head div h1")
    post_title       = get_text(session, ".post h1")
    post_body        = get_text(session, ".post .copy")
    post_footer      = get_text(session, ".post aside")

    assert index_h1_heading =~ ~r/Today I Learned/i
    assert post_title       =~ ~r/Example Title/
    assert post_body        =~ ~r/Example Body/
    assert post_footer      =~ ~r/#phoenix/i
  end
end

And the final test run:

$ mix test

.

Finished in 4.5 seconds
1 test, 0 failures

Randomized with seed 433617

Don't be alarmed by the 4.5 second run time. At the time of this post's publication, Tilex has twenty-two unit and integration tests, and the entire suite often runs about that fast on a normal laptop. Setup and teardown is the big cost.

Conclusion

If you haven't read Plataformatec's post about integration testing with Hound, check it out. Both Hound and Wallaby seem great.

Integration testing will be a big part of developing in Phoenix, as developers build more and more complex applications. I hope this post gave you a new tool for developing your Phoenix apps from the supportive harness of a test suite. Please let me know if you've used Wallaby, and how it worked for you.

Photo Credit: NASA/Landsat8, Acadia National Park, flickr.com, https://www.flickr.com/photos/gsfc/29143744581. Accessed 2 January 2017.

Posted in Elixir