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}
]
endRun 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.githubCoverage 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.htmlThe 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
endOr a single line:
raise "should never happen" # coveralls-ignoreCI 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.githubmix 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):
- Sign up at coveralls.io and connect your repository
- Get your repo token from the Coveralls dashboard
- 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 --umbrellaConfigure 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:
- Uncovered error paths —
rescueblocks,{:error, _}returns, exception handlers - Uncovered branches —
cond,case,withbranches that tests don't exercise - Dead code — functions nobody calls (remove them)
- 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.