Excoveralls: Test Coverage Reports for Phoenix and Elixir in CI

Excoveralls: Test Coverage Reports for Phoenix and Elixir in CI

Excoveralls generates test coverage reports for Elixir projects. It integrates with Coveralls.io for PR coverage comments, generates HTML reports for local review, and produces LCOV format for other coverage tools. Setting up coverage reporting early helps maintain quality as your Phoenix application grows.

Installation

# mix.exs
def project do
  [
    # ...
    test_coverage: [tool: ExCoveralls],
    preferred_cli_env: [
      coveralls: :test,
      "coveralls.detail": :test,
      "coveralls.post": :test,
      "coveralls.html": :test,
      "coveralls.github": :test
    ]
  ]
end

defp deps do
  [
    {:excoveralls, "~> 0.18", only: :test}
  ]
end

Run mix deps.get to install.

Running Coverage

# Console output with summary
mix coveralls

<span class="hljs-comment"># Detailed output showing uncovered lines
mix coveralls.detail

<span class="hljs-comment"># HTML report (opens in browser)
mix coveralls.html

<span class="hljs-comment"># Send to Coveralls.io
mix coveralls.post

<span class="hljs-comment"># GitHub Actions integration (annotates PRs)
mix coveralls.github

Coverage Output Formats

Console Output

COV    FILE                                        LINES RELEVANT   MISSED
100.0% lib/my_app/accounts/user.ex                   45       12        0
 87.5% lib/my_app/accounts/user_service.ex            80       24        3
 62.5% lib/my_app/payments/stripe.ex                  92       32       12
 ...

[TOTAL]  84.2%

Detailed Output

mix coveralls.detail --filter "stripe"

Shows every uncovered line:

----------------
COV    FILE                                        LINES RELEVANT   MISSED
 62.5% lib/my_app/payments/stripe.ex                  92       32       12
----------------
...
  45: def handle_webhook(%{"type" => "payment_failed"} = event) do
  46:   # NOT COVERED
  47:   charge_id = get_in(event, ["data", "object", "id"])
...

HTML Report

mix coveralls.html
open cover/excoveralls.html

The HTML report provides a browsable interface showing covered (green) and uncovered (red) lines for every file.

Configuration

Create coveralls.json in your project root:

{
  "coverage_options": {
    "minimum_coverage": 80,
    "treat_no_relevant_lines_as_covered": true,
    "output_dir": "cover/",
    "template_path": "custom_template.html.eex"
  },
  "skip_files": [
    "test/",
    "lib/my_app_web/telemetry.ex",
    "lib/my_app/release.ex",
    "lib/my_app/repo.ex",
    "priv/"
  ],
  "minimum_coverage": 80
}

Skipping Files and Lines

Some code is difficult or inappropriate to test (migration files, generated code, router boilerplate). Skip them:

{
  "skip_files": [
    "test/",
    "lib/my_app_web/router.ex",
    "lib/my_app/application.ex",
    "priv/repo/migrations/",
    "lib/my_app_web/gettext.ex"
  ]
}

Skip individual lines with comments:

def start_link(opts) do
  # coveralls-ignore-start
  GenServer.start_link(__MODULE__, opts, name: __MODULE__)
  # coveralls-ignore-stop
end

Or a single line:

raise "should never happen" # coveralls-ignore

CI Integration

GitHub Actions

# .github/workflows/test.yml
name: Test

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_USER: postgres
          POSTGRES_PASSWORD: postgres
          POSTGRES_DB: my_app_test
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 5432:5432

    steps:
      - uses: actions/checkout@v4

      - uses: erlef/setup-beam@v1
        with:
          elixir-version: "1.16"
          otp-version: "26"

      - name: Install dependencies
        run: mix deps.get

      - name: Run tests with coverage
        env:
          MIX_ENV: test
          DATABASE_URL: postgresql://postgres:postgres@localhost/my_app_test
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: mix coveralls.github

mix coveralls.github posts a coverage report as a PR check and leaves a comment with the coverage summary.

Coveralls.io Integration

For Coveralls.io (free for open source):

  1. Sign up at coveralls.io and connect your repository
  2. Get your repo token from the Coveralls dashboard
  3. Add to CI:
- name: Run tests with coverage
  env:
    COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}
  run: mix coveralls.post --branch ${{ github.ref_name }}

Coveralls shows coverage trends over time and adds PR comments with coverage diff.

Enforcing Coverage Minimums

Set a minimum coverage threshold that fails CI if coverage drops:

{
  "coverage_options": {
    "minimum_coverage": 80
  }
}
$ mix coveralls
...
[TOTAL]  72.4%

Coverage is below minimum threshold of 80%

The command exits with a non-zero status code, failing CI. This prevents coverage regressions from slipping into main.

Per-File Minimums

For critical modules, enforce higher coverage:

# In mix.exs test_coverage config
test_coverage: [
  tool: ExCoveralls,
  # custom per-module minimums not built-in, but you can use custom reporters
]

Alternatively, use Credo or custom mix tasks to enforce per-module coverage.

Coverage in Phoenix Applications

Phoenix generates significant boilerplate. Configure skips to focus coverage reports on your business logic:

{
  "skip_files": [
    "test/",
    "lib/my_app_web/telemetry.ex",
    "lib/my_app_web/gettext.ex",
    "lib/my_app/application.ex",
    "lib/my_app_web/endpoint.ex",
    "lib/my_app/repo.ex",
    "priv/repo/migrations/",
    "lib/my_app_web/router.ex"
  ]
}

This focuses coverage on contexts, schemas, controllers, live views, and service modules — where your business logic lives.

Umbrella Projects

For umbrella apps, run coverage per app or aggregate:

# Coverage for a specific app
<span class="hljs-built_in">cd apps/my_core && mix coveralls

<span class="hljs-comment"># Aggregate umbrella coverage
mix coveralls --umbrella

Configure per-app skips in each app's coveralls.json.

Reading Coverage Reports Effectively

Coverage percentage is a lagging indicator — high coverage doesn't mean good tests. Use coverage reports to find:

  1. Uncovered error pathsrescue blocks, {:error, _} returns, exception handlers
  2. Uncovered branchescond, case, with branches that tests don't exercise
  3. Dead code — functions nobody calls (remove them)
  4. Integration gaps — modules with 0% coverage (no tests at all)

Don't chase 100% coverage. Focus on covering the code that matters: business logic, error handling, and security-sensitive paths.

Combining Coverage with Production Monitoring

Coverage shows what your tests exercise. HelpMeTest shows what your production users experience. Use coverage to ensure your code is tested pre-deploy, and HelpMeTest to verify it actually works in production 24/7.

Summary

Excoveralls integrates with Phoenix CI pipelines with minimal configuration. Run mix coveralls.html locally for detailed reports, mix coveralls.github in CI for PR comments, and enforce minimums with the minimum_coverage option.

Configure skip_files to exclude boilerplate from coverage calculations, and use coverage reports as a guide to find gaps in test coverage — particularly around error paths and conditional branches.

Read more