Testing Elixir Apps on GitLab

Photo credit: Unsplash: _louisreed

Recently, I started a new side project (maybe more on that soon), and I decided to give GitLab a try. I’ve used it for some client work through my job, but had never really messed with it on my own.

Some of my motivations for giving it a try:

The biggest reason here is the free hours of Continuous Integration (CI). There are several guides for setting up testing for an Elixir Project, but my setup ended up a little different, so I figured I’d throw up a post about it (and have it as a reference for myself, for the future, one of the best reasons to blog!)

GitLab CI Config

Everything starts with a .gitlab-ci.yml file:

image: elixir:1.7.3

services:
  - postgres:9.6

variables:
  POSTGRES_DB: app_name_test
  POSTGRES_HOST: postgres
  POSTGRES_USER: postgres
  POSTGRES_PASSWORD: "postgres"
  MIX_ENV: "test"

This part is pretty straight forward, just set the running to use the right version of Elixir, connect PostgreSQL (if needed) and set up a few global variables

test.exs

use Mix.Config

# We don't run a server during test. If one is required,
# you can enable the server option below.
config :app_name, Web.Endpoint,
  http: [port: 4001],
  server: false

# Print only warnings and errors during test
config :logger, level: :warn

# Configure your database
config :app_name, AppName.Repo,
  adapter: Ecto.Adapters.Postgres,
  username: System.get_env("POSTGRES_USER") || "postgres",
  password: System.get_env("POSTGRES_PASSWORD") || "postgres",
  database: System.get_env("POSTGRES_DB") || "app_name_test",
  hostname: System.get_env("POSTGRES_HOST") || "localhost",
  pool: Ecto.Adapters.SQL.Sandbox

This is set up to mostly just pull the env variables from the CI run, or default to something reasonable so that your tests will still run locally.

Global Setup

Next up in the .gitlab-ci.yml file:

before_script:
  - mix local.hex --force
  - mix local.rebar --force

Here we are making sure that every job that runs (each stage of the pipeline) has hex and rebar ready to go. This is important when we add caching later, as these install outside the project directory.

Stages

Now we will add stages to the .gitlab-ci.yml file:

compile:
  stage: build
  script:
    - apt-get update && apt-get -y install postgresql-client
    - mix deps.get --only test
    - mix compile --warnings-as-errors

test:
  stage: test
  script:
    - mix ecto.create
    - mix ecto.migrate
    - mix test

lint:
  stage: test
  script:
    - mix format --check-formatted
    - mix credo

The setup here runs the compile job in the build stage, so this will happen first. We’ll make sure we are ready with the postgres client, all the dependices for the app, and we’ll compile all our elixir code. I set --warnings-as-errors to make sure I’m not leaving anything deprecated or unused behind in my code.

The test stage has two jobs, test and lint. These will be able to run in parallel on GitLab’s servers, or any other connected runner. The test job setup up ecto, and runs the test suite. The link job makes sure everything is formatted, and clears a credo check.

Here is the display from GitLab for the pipeline stages:

And the pipeline jobs:

Caching

Lastly, we want builds to go faster, so we add a block for caching configuration:

cache:
  paths:
    - _build
    - deps
    - assets/node_modules

This way our dependecy download, build output and anything with our node modules in assets are preserved and not built from scratch each time. Mix and Yarn should handle if your dependencies need updating due to a change you’ve made.

Conclusion

This approach seems to be working great! For my really simple starting Phoenix app with just a few tests, the original build took about 4.5 minutes, and each stage now runs in about 1.75 minutes. That is of course a lot more time than the tests take to run themselves, but there is a lot of overhead to get the tests ready to run. The formatting and credo runs (and tests and compile without warnings) I also do myself with git pre-commit hooks (I should do a post on that), so its unlikely something wrong would slip in, but I like the redundancy, and having this automated is a lot of fun!

Here is the GitLab interface for a successful pipeline run:

Happy CI-ing!

Here is the whole file for reference:

image: elixir:1.7.3

services:
  - postgres:9.6

variables:
  POSTGRES_DB: app_name_test
  POSTGRES_HOST: postgres
  POSTGRES_USER: postgres
  POSTGRES_PASSWORD: "postgres"
  MIX_ENV: "test"

cache:
  paths:
    - _build
    - deps
    - assets/node_modules

before_script:
  - mix local.hex --force
  - mix local.rebar --force

compile:
  stage: build
  script:
    - apt-get update && apt-get -y install postgresql-client
    - mix deps.get --only test
    - mix compile --warnings-as-errors

test:
  stage: test
  script:
    - mix ecto.create
    - mix ecto.migrate
    - mix test

lint:
  stage: test
  script:
    - mix format --check-formatted
    - mix credo