Elixir/Phoenix with GitlabCI

July 13, 2017 by mtchavez

Gitlab has been approaching the hosted VCS solution with a more robust and thought out handling of the entire release pipeline for code instead of the basic features of a git flow approach. Enter GitlabCI.

GitlabCI is just one piece of the modern release pipeline that is offered from Gitlab. Another modern piece of technology in Software Engineering has also been Elixir, the functional programming language built on top of Erlang and OTP with a ruby-inspired syntax. The creators of the language came form the ruby and rails world and have also made a web framework called Phoenix as a good introduction to the power of Elixir.

So how can we move into the modern development landscape and bring these two worlds together, and will it bring the fuzzy warm feelings? In this post we will:

  1. Look into some setup needed for Elixir and/or Phoenix to run tests
  2. Getting your test coverage
  3. Running a linter against your code
  4. Setting up a project with GitlabCI
  5. Tying it all together to get your project running through GitlabCI

Setting up testing

This assumes some prior knowledge of Phoenix and Elixir as well as some of the tooling around the two like Hex and Mix. We will start out by creating a new Phoneix app and running the tests.

$ mix phoenix.new testly
$ cd testly
$ mix test

If all went well you should see some output similar to:

....

Finished in 0.06 seconds
4 tests, 0 failures

Randomized with seed 388184

So as you can see testing is a first class citizen with both Elixir and Phoenix. If you’ve come from other languages there is usually a separate step for test framework setup and writing tests with additional tools for mocking, stubbing, running in a browser etc. Elixir has ExUnit which Phoenix uses so test setup here will be simply generating a new Phoenix app.

Test coverage

Once your app has tests you are bound to wonder how much of your code is being covered and when someone adds new code without tests you can see some coverage metric go down. Generating test coverage is another simple thing here. One theme with Elixir is it leverages a lot of existing Erlang solutions and tools, here is no exception. From the test help output under coverage you can see:

# Coverage
[...]
By default, a very simple wrapper around OTP's cover is used

So running mix test --cover will generate fairly rudimentary html files for your code where you can see un-covered lines. Because this is a little rough for getting an overall coverage we will utilize a Hex Elixir package to get a proper coverage output.

If you open up mix.ex you can add excoveralls to the dependencies as so:

defp deps do
  [
    {:phoenix, "~> 1.2.4"},
    {:phoenix_pubsub, "~> 1.0"},
    {:phoenix_ecto, "~> 3.0"},
    {:postgrex, ">= 0.0.0"},
    {:phoenix_html, "~> 2.6"},
    {:phoenix_live_reload, "~> 1.0", only: :dev},
    {:gettext, "~> 0.11"},
    {:cowboy, "~> 1.0"},

    # Test

    {:excoveralls, "~> 0.7", only: :test}
  ]
end

And add some stup to the project in mix.ex as well

def project
  [app: :testly,
    [...]
    # Test
    test_coverage: [tool: ExCoveralls],
    preferred_cli_env: [
      "coveralls": :test,
      "coveralls.detail": :test,
      "coveralls.post": :test,
      "coveralls.html": :test
    ],
    [...]
  ]
end

You can read more on the ExCoveralls project page on how to set up your app.

Now we can run mix coveralls to get a test output with a total coverage as so

Finished in 0.08 seconds
4 tests, 0 failures

Randomized with seed 276987
----------------
COV    FILE                                        LINES RELEVANT   MISSED
 75.0% lib/testly.ex                                  31        4        1
  0.0% lib/testly/endpoint.ex                         42        0        0
  0.0% lib/testly/repo.ex                              3        0        0
  0.0% test/support/channel_case.ex                   43        4        4
100.0% test/support/conn_case.ex                      44        4        0
  0.0% test/support/model_case.ex                     65        6        6
  0.0% web/channels/user_socket.ex                    37        0        0
100.0% web/controllers/page_controller.ex              7        1        0
  0.0% web/gettext.ex                                 24        0        0
 50.0% web/router.ex                                  26        2        1
  0.0% web/views/error_helpers.ex                     40        5        5
100.0% web/views/error_view.ex                        17        1        0
  0.0% web/views/layout_view.ex                        3        0        0
  0.0% web/views/page_view.ex                          3        0        0
  0.0% web/web.ex                                     81        1        1
[TOTAL]  35.7%
----------------

Linting your code

If you aren’t a fan of linters for your code or making sure there are some consistent code styles enforced you can skip to the next part. I have been using the Dogma package to check code so add it to your dependencies first:

defp deps do
  [
    # Dev
    {:dogma, "~> 0.1", only: :dev}
  ]
end

And run with mix dogma. There should be a handful of errors from a fresh Phoenix project so you can choose to configure Dogma to ignore certain rules or fix them yourself. See the configuration doc for more information.

Integration with GitlabCI

A good starting point will be the docs to familiarize yourself with what GitlabCI can do. If you’ve used other services like [TravisCI][travis] or [CircleCI][circle] you should be familiar with the YAML configuration approach and containerized builds that can be parallelized. With that let’s add a .gitlab-ci.yml file to the project with the following:

image: elixir:1.4
services:
  - postgres:9.6
variables:
  MIX_ENV: "test"
before_script:
  - apt-get update
  - apt-get install -y postgresql-client
  - mix local.hex --force
  - mix local.rebar --force
  - mix deps.get --only test
  - mix ecto.reset
test:
  script:
    - mix test

This setup is configuring GitlabCI to use the elixir:1.4 docker image for our tests to run in. The services section is configuring the required services dependencies for our build, in this case Postgres for Phoenix and Ecto. We are also setting the environment for Mix to be in test mode. Other than that we have a handful of commands in the before_script that set up the test environment to get all the dependencies and reset the test DB with mix ecto.reset.

If you check this in from a feature branch and open a merge request against your repository you should see your GitlabCI build in the pipeline. If all goes well there will be a successful build.

Tying Everything Together with GitlabCI

The basic example is nice since it is fairly simple and will get us running tests with GitlabCI but you may be wondering why there is no test coverage or linting going on since we made sure to set all those things up earlier. GitlabCI allows you to create fairly complex build rules and we will only scratch the surface with adding coverage and linting to our basic example.

If you are following along with the code examples you can replace the .gitlab-ci.yml with this:

variables:
  MIX_ENV: "test"

stages:
  - build
  - test
  - post-test

.job_template: &job_definition
  image: elixir:1.4
  before_script:
    - apt-get update
    - apt-get install -y postgresql-client
    - mix local.hex --force
    - mix local.rebar --force
    - mix deps.get --only test

test:
  <<: *job_definition
  stage: test
  services:
    - postgres:9.6
  script:
    - "mix ecto.reset"
    - "mix coveralls.html | tee cov.out"
  artifacts:
    paths:
      - cov.out
      - cover/

dogma:
  <<: *job_definition
  stage: test
  script:
    - mix dogma
  variables:
    MIX_ENV: "dev"

coverage:
  image: alpine
  stage: post-test
  script:
    - cat cov.out
  coverage: '/\[TOTAL\]\s+(\d+\.\d+%)$/'

Things suddenly got a lot more involved but really we are doing a lot here with still a small amount of code, even being DRY with some YAML tricks. Briefly lets go through the changes here. First we have replaced the top level configuration with jobs for test, dogma, and coverage. GitlabCI treats each one of these as a separate job. You may have noticed there is also a new key of stages. Jobs can run in specific stages and jobs in the same stage will all run in parallel at the same time. So what we have done here is say we have two jobs running in our test stage, the first being the default test job and a custom job of dogma to run our linting. Our last job is coverage which is running in a post-test stage which will wait for all the test jobs to have run before gathering the coverage.

Another change here is we are re-using some setup code with our job_template to set up the test jobs with the same image and before script. This allows us to share a common environment across jobs. Our test job is also using the postgres service, resetting the test DB, and exporting coverage artifacts to be used by later stages, namely the coverage job.

Once the test stage has finished the coverage job will run and have access to our coverage artifacts from the tests build. We use that to tell GitlabCI how to parse our coverage output. If everything passes you should be able to add some badges to your README for build status and coverage as so

[![build status](https://gitlab.com/your-user/testly/badges/master/build.svg)](https://gitlab.com/your-user/testly/commits/master)
[![coverage report](https://gitlab.com/your-user/testly/badges/master/coverage.svg)](https://gitlab.com/your-user/testly/commits/master)

Wrapping Up

As you can see both Elixir and GitlabCI are fairly easy to integrate together. With Elixir and Phoenix there is a lot out of the box as well as some basic packages with some modern development practices. GitlabCI shows some promise with simplicity as well as more complex build pipelines. There is even more that is possible to go deeper into the release cycle of code on Gitlab using review apps, docker registry integration, and continuous deployment.

mtchavez All rights reserved.