Phoenix: Integration tests at GitHub actions

Tutorial to create integration tests and add them to GitHub Actions

In this post, I will try to explain how to do integration tests for your Phoenix project and add them to the checks in your repository

These are the tools that we will use:

  • Ex_machina to create test data.
  • Wallaby to test your web applications by simulating realistic user interactions.
  • GitHub Actions to configure the environment and run the tests at GitHub.

ExMachina

The first step will be to configure your project to easily create test data. As mentioned above, we will use ex_machina. To install it follow the instructions of their repository. Once we have installed the library, let's configure it to our needs.

Factories and Factory Module

We will split factories into separate files and will import them by creating a Factory module.

Here we have the code of our Client factory.

# test/suport/factories/client.ex

defmodule OurProject.ClientFactory do
  defmacro __using__(_opts) do
    quote do
      def client_factory do
        %OurProject.Clients.Client{
          name: "Client Name",
          contact_info: "Client contact_info"
        }
      end
    end
  end
end

And then we import it in our Factory Module.

# test/support/factory.ex

defmodule OurProject.Factory do
  use ExMachina.Ecto, repo: OurProject.Repo
  use OurProject.ClientFactory
end

Once we have created our Factory module and our factories we can use them in any test importing the factory module. You can with ExMachina you can build or insert test data, the main difference is that build returns an object that is not saved at the database. For more information read the Overview section of their repository

Here is an example test that inserts a client when we need it. Calling the client_fixture function.

# test/our_project/client/some_client_test.ex

defmodule OurProject.SomeClientTest do
  use OurProject.DataCase

  import OurProject.Factory

  def client_fixture(attrs \\ %{}) do
    # `insert*` returns an inserted comment. Only works with ExMachina.Ecto
    # Associated records defined on the factory are inserted as well.
    insert(:client, attrs)
  end
	...
end

Wallaby

We already have our data so we can start with the integration tests. Install wallaby following the instructions on their GitHub page and configure it with Chromedriver.

# config/test.exs

config :wallaby,
  driver: Wallaby.Experimental.Chrome,
  chrome: [
    headless: true
  ]

Now, we will create a FeatureCase module to configure the database, create the session and import the modules and helpers that our tests will need. Here we will import our Factory module.

# test/support/feature_case.ex

defmodule OurProject.FeatureCase do
  use ExUnit.CaseTemplate

  using do
    quote do
      use Wallaby.DSL
      import Wallaby.Query

      alias OurProject.Repo
      import Ecto
      import Ecto.Changeset
      import Ecto.Query

      #Import our Router Helpers
      import OurProject.Router.Helpers

      #Import custom helpers if we need them
      import OurProject.TestHelpers.Navigation

      #Import our ExMachina factories
      import OurProject.Factory
    end
  end

  # Connect to the test database and create the session for each test
  setup tags do
    :ok = Ecto.Adapters.SQL.Sandbox.checkout(OurProject.Repo)

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

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

Finally, we are ready to create our first test. We are going to create a test that requires fake data so we can see how to use the factories inside an integration test.

#test/integration/client/delete_client.exs

defmodule OurProject.DeleteClientTest do
  use OurProject.FeatureCase, async: false

  setup %{session: session} do
    client = insert(:client)
    {:ok, session: session, client: client}
  end

  test "remove client", %{session: session, client: client} do
    session
    |> visit("/clients", client.name)
    |> click(link(client.name))
    |> click(link("delete"))
    |> assert_has(css(".toast-message", count: 1, text: "Client successfully deleted"))
  end
end

GitHub Actions

We have our integration tests and we want to check them when we submit a pull request. With a few steps, we can configure our GitHub workflow to run the tests every time we push to the branch.

Chromedriver

We have to add Chromedriver to our CI. To do it we will use an action created by @nanasses. Here you can read the documentation.

# .github/workflows/ci.yml

- uses: nanasess/setup-chromedriver@master

Compile our project

We have to get the dependencies, compile the project and run the migrations.

# .github/workflows/ci.yml

- name: Install Dependencies
  run: mix deps.get
- run: mix compile
- run: mix ecto.migrate

Compile our assets

In order to have our javascript compiled it's needed to compile our assets.

# .github/workflows/ci.yml

- run: npm install
  working-directory: ./assets
- run: npm run deploy --prefix ./assets

Run the tests

And finally, run the tests.

# .github/workflows/ci.yml

- run: mix test --trace

Overview

There is our entire ci.yml file.

name: Continuous Integration
on: [push]

# This workflow will build and test a Phoenix application that uses
# PostgreSQL as a database. Will also cache dependencies and build
# artifacts to speed up the build.
jobs:
  test:
    env:
      MIX_ENV: ci
      DATABASE_URL: postgres://postgres:postgres@localhost:5432/postgres
    runs-on: ubuntu-latest

    services:
      db:
        env:
          POSTGRES_USER: postgres
          POSTGRES_PASSWORD: postgres
          POSTGRES_DB: postgres
        image: postgres:11
        ports: ['5432:5432']
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
    steps:
      - uses: actions/checkout@v1.0.0
      - name: Recover dependency cache
        uses: actions/cache@v1
        id: deps_cache
        with:
          path: deps
          key: ${{ runner.OS }}-deps-${{ hashFiles('**/mix.lock') }}
          restore-keys: |
            ${{ runner.OS }}-deps-${{ env.cache-name }}-
            ${{ runner.OS }}-deps-
            ${{ runner.OS }}-
      - name: Recover build cache
        uses: actions/cache@v1
        with:
          path: _build
          key: ${{ runner.OS }}-build-${{ env.cache-name }}
          restore-keys: |
            ${{ runner.OS }}-build-
            ${{ runner.OS }}-
      - uses: actions/setup-elixir@v1.0.0
        with:
          otp-version: 22.x
          elixir-version: 1.9.x
      - uses: nanasess/setup-chromedriver@master
      - name: Install Dependencies
        run: mix deps.get
      - run: mix compile
      - run: mix ecto.migrate
      - run: npm install
        working-directory: ./assets
      - run: npm run deploy --prefix ./assets
      - run: mix test --trace

That's it

We have our tests created and running on GitHub. If you are interested in GitHub Actions, at Codegram we have a repository with GitHub Actions workflows that can be useful to you.

I hope you found this post useful. If you have any comments, questions or suggestions, tell us on Twitter!

Photo by Glenn Carstens-Peters on Unsplash