This tutorial was initially published on DailyDrip. Read the original post here.
Introduction
What’s the point of writing tests if you aren’t going to ensure they’re always passing? It’s important to set up a continuous integration service very early in a project. In this tutorial, you will learn how to set up continuous integration for an Elixir project using Semaphore. The project we’ll be working on is Firestorm, an open-source forum engine, with an Elixir Phoenix backend, and an Elm frontend.
You can either watch the video tutorial, or keep reading for a text version.
The Project
We’re starting with the dailydrip/firestorm repo tagged before this episode.
We’ll make a new branch, for later:
git checkout -b feature/semaphore_test
git push origin feature/semaphore_test
Setting Up Continuous Integration
Setting up Semaphore is not particularly difficult. First, we’ll sign up or log into our Semaphore account, where we’re presented with a list of our projects. We’ve already connected our GitHub account – you would have to go through that step first.
Next, we’ll click Add new project to add Firestorm:
Then we’ll pick the branch to build – we want to build the semaphore_test branch:
Finally, we pick the owner of the project:
Now, Semaphore will analyze the project, identify it as an Elixir project, and ask us to confirm the build steps:
We’ll ensure the latest version of Elixir is being used and apply these build steps. If we look, we can see that it hangs because there’s no rebar3
and it wants us to confirm that it should install it, which of course we can’t do as this is not interactive.
Could not find "rebar3", which is needed to build dependency :idna
I can install a local copy which is just used by Mix
Let’s modify our build steps to first install rebar3
, by adding the following step:
mix local.rebar --force
Now, if we run the build again, it will fail because it can’t connect to the database. Semaphore gives us a couple of environment variables that are meant to help here. First, let’s move them to environment variable names of our choosing in Semaphore itself, in the build steps:
export POSTGRES_USER=$DATABASE_POSTGRESQL_USERNAME
export POSTGRES_PASSWORD=$DATABASE_POSTGRESQL_PASSWORD
We’ll also need to set some environment variables, in the appropriate section in Semaphore’s admin:
export AWS_ACCESS_KEY_ID=xxxxxxxx
export AWS_SECRET_ACCESS_KEY=xxxxxxxxx
export AWS_S3_BUCKET=test-firestormforum-org
export AWS_S3_REGION=us-west-2
mix test
Then, we’ll update firestorm’s test configuration to support using these environment variables for the repo’s database setup:
vim config/test.exs
# Configure your database
config :firestorm_web, FirestormWeb.Repo,
adapter: Ecto.Adapters.Postgres,
username: System.get_env("POSTGRES_USER") || "postgres",
password: System.get_env("POSTGRES_PASSWORD") || "postgres",
database: "firestorm_web_test",
hostname: "localhost",
pool: Ecto.Adapters.SQL.Sandbox
If we push this up, we’ll see a new build triggered. This build will likely fail due to a race condition involving our Notifications worker. Let’s modify the application to avoid starting our Notifications process in test mode:
vim lib/firestorm_web/application.ex
defmodule FirestormWeb.Application do
# We need the whole module to have access to the supervisor spec functions
import Supervisor.Spec
# ...
def start(_type, _args) do
opts = [strategy: :one_for_one, name: FirestormWeb.Supervisor]
Supervisor.start_link(children(Mix.env), opts)
end
defp default_children() do
[
# Start the Ecto repository
supervisor(FirestormWeb.Repo, []),
# Start the endpoint when the application starts
supervisor(FirestormWeb.Web.Endpoint, [])
]
end
defp children(:test) do
default_children()
end
defp children(_) do
default_children() ++
[
# Start the notifications server
worker(FirestormWeb.Notifications, [])
]
end
end
We’d like to start it for the duration of the notifications test, however:
vim test/notifications_test.exs
defmodule FirestormWeb.NotificationsTest do
# ...
setup do
{:ok, _} = FirestormWeb.Notifications.start_link()
:ok
end
# ...
end
With that, our tests should pass. They still don’t quite, on the CI server. This seems to happen because of an issue when we redirect. This is probably due to a bug in the version of phantomjs that is being used on Semaphore at present. When phantomjs redirects, it fails to send the appropriate headers to keep us in the right Ecto sandbox. For now, we’ll skip the thread feature test’s watch
tests, as those are what are triggering the redirects.
vim test/feature/threads_test.exs
# ...
@tag :pending
test "creating a new thread", %{session: session} do
# ...
@tag :pending
test "creating a new thread when unauthenticated", %{session: session} do
# ...
@tag :pending
test "watching a thread", %{session: session} do
# ...
And we’ll configure ExUnit
to skip pending tests:
vim test/test_helper.exs
ExUnit.configure(exclude: [pending: true])
ExUnit.start()
# ...
We’ll push this up, and the tests should pass.
Summary
In this tutorial, we saw how to set up continuous integration using Semaphore CI for the Firestorm project. Aside from a simple tweak to support the environment variables that we’re provided by Semaphore, it was completely trivial.
Feel free to leave any comments or questions you may have in the comment section below.