24 Jan 2017 · Software Engineering

    Integration Testing Phoenix with Wallaby

    9 min read
    Contents

    Post originally published on https://hashrocket.com. Republished with author’s permission.

    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.

    Leave a Reply

    Your email address will not be published. Required fields are marked *

    Avatar
    Writen by:
    Jake is developer at Hashrocket currently porting a Ruby on Rails application to Phoenix. Find him on Twitter and his blog.