Contents

Running gotest locally is straightforward. Making it work well in CI — with clear failure output, PR annotations, coverage enforcement, and safety guards — takes a bit of setup. This post covers the official GitHub Action, the summary command, coverage thresholds, and the CI mode that catches debugging artifacts before they reach your main branch.

If you haven’t written a gotest suite yet, start with Your First Go Test Suite in 10 Minutes and come back — everything here builds on that workflow.

The problem with go test -v in CI

The default go test -v output is verbose. For every test, it prints two or three lines: === RUN, === CONT (for parallel tests), and --- PASS or --- FAIL. In a 200-test suite, that is 400–600 lines of output. When two tests fail, the failure messages are buried in the middle of hundreds of lines of passing noise.

Most teams work around this by piping output through grep or gotestfmt. gotest has a built-in answer: the summary command.

gotest summary: failure-focused output

The summary subcommand filters test output to show only what matters. When all tests pass, it prints a single line:

all tests pass
147 tests passed (2.3s)
Coverage: 82.4%

When tests fail, it shows only the failing tests with their assertion output — no === RUN, no === CONT, no --- PASS noise:

failures show assertion output only
3 of 147 tests failed

FAIL  pkg/foo TestValidateInput / empty string (12ms)
      foo_test.go:42: expected error, got nil

FAIL  pkg/bar TestProcessOrder / concurrent writes (1.2s)
      bar_test.go:88:
        expected: []string{"a", "b", "c"}
             got: []string{"a", "c", "b"}

A developer reading this output sees exactly which tests failed, where, and why. No scrolling required.

The GitHub Action

gotest includes a composite GitHub Action at mvrahden/go-test@v1. It wraps gotest summary --github and handles installation, test execution, coverage reporting, and annotations in one step.

Here is a minimal workflow:

.github/workflows/test.yml
name: test

on:
  pull_request:
  push:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version-file: go.mod

      - uses: mvrahden/go-test@v1
        with:
          packages: ./...
          race: true
          coverage: true
          min-coverage: 80

That is the entire CI setup. The action does the following:

  1. Installs gotest. By default (version: gomod), it runs gotest through go run, resolving the version from your project’s go.mod. This keeps the CI version in sync with local development — no version drift.
  2. Runs tests with gotest summary --github. The --github flag is auto-detected in GitHub Actions via the $GITHUB_ACTIONS environment variable.
  3. Reports failures as GitHub ::error annotations. These appear inline on the PR diff, pointing at the exact file and line of each failing assertion.
  4. Writes a step summary to the job summary panel with pass/fail counts and coverage.
  5. Enforces coverage. If min-coverage is set and the coverage percentage falls below the threshold, the step fails.

Action inputs

The action accepts these inputs:

NameDefaultDescription
packages./...Package patterns to test.
racefalseEnable the race detector.
coveragefalseEnable coverage profiling and reporting.
min-coverageMinimum coverage percentage (0–100). Fails the step if below.
flagsAdditional gotest flags (--double-dash style).
go-test-flagsAdditional go test flags (-single-dash style).
versiongomodgotest version: gomod resolves from go.mod, or a version tag (e.g. v1.0.0, latest) to install globally.

Action outputs

The action exposes two outputs for downstream steps:

NameDescription
exit-codeThe test process exit code.
coverageThe coverage percentage (empty if coverage is not enabled).

You can use these in subsequent steps, for example to post a coverage comment or gate a deployment:

.github/workflows/test.yml (continued)
      - uses: mvrahden/go-test@v1
        id: test
        with:
          packages: ./...
          coverage: true

      - name: Coverage gate
        if: steps.test.outputs.coverage != ''
        run: echo "Coverage: ${{ steps.test.outputs.coverage }}%"

Version resolution

The default version: gomod strategy runs gotest via go run github.com/mvrahden/go-test/cmd/gotest. This resolves the version pinned in your project’s go.mod, which means CI runs the exact same version you use locally. No separate install step, no version drift.

For this to work, gotest needs to be in your go.mod. The cleanest way is a tool directive (Go 1.24+):

go.mod
module your-project

go 1.25

tool github.com/mvrahden/go-test/cmd/gotest

If your project does not depend on gotest as a library, set version: latest or a specific tag (e.g. v1.2.0) to install a standalone binary via go install instead.

CI mode and the focus-prefix guard

gotest has a CI mode that activates two safety behaviors:

  1. Focus-prefix guard. If any suite or method has an F_ prefix (F_TestSomething, F_MyTestSuite), the build fails immediately. The F_ prefix means “run only this” — useful during development, dangerous in CI. Without the guard, a committed F_ prefix silently skips every other test in the package.
  2. Snapshot read-only mode. Snapshot baselines cannot be created in CI. Locally, a first run writes the missing baseline file; in CI mode, a missing baseline fails the test instead — so a brand-new snapshot can never slip in unreviewed. (Mismatches fail in any mode; updating a baseline always requires an explicit --update-snapshots run.)

How focus prefixes fit into day-to-day development is covered in depth in the inner loop post.

CI mode activates automatically when the CI environment variable is set, which GitHub Actions, GitLab CI, CircleCI, and most other CI systems do by default. You can also enable it explicitly with --ci:

gotest --ci ./...

To opt out when CI is set (rare, but occasionally useful for CI debugging), set GOTEST_CI=0.

The focus-prefix guard is important enough to deserve emphasis: without it, a developer who forgets to remove F_TestLogin before pushing effectively disables every other test in that package. The suite passes because the one focused test passes. The 49 skipped tests are invisible in the output. CI mode turns this silent skip into a loud failure.

Coverage thresholds

gotest calculates statement-weighted coverage from Go’s built-in coverage profile. You can set a minimum threshold that fails the build if coverage drops below it:

via the GitHub Action
- uses: mvrahden/go-test@v1
  with:
    coverage: true
    min-coverage: 80
via the CLI
gotest --min=80 ./...
via .gotest.yml (checked into the repo)
min-coverage: 80

All three methods have the same effect. Using .gotest.yml has the advantage that the threshold is checked in both local development and CI without passing flags.

Multi-platform testing

Use a matrix strategy to test across Go versions and operating systems:

.github/workflows/test.yml
jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
        go-version: ["1.25", "1.26", "1.27"]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: ${{ matrix.go-version }}

      - uses: mvrahden/go-test@v1
        with:
          packages: ./...
          race: true

The action works on all three platforms. Each matrix entry gets its own failure annotations and step summary.

Non-GitHub CI systems

For GitLab CI, CircleCI, or any other system, run gotest summary directly. CI mode is auto-detected from the CI environment variable (which most CI systems set). The --github flag for annotations is auto-detected from $GITHUB_ACTIONS, so it stays inactive on other platforms.

.gitlab-ci.yml
test:
  image: golang:1.25
  script:
    - go install github.com/mvrahden/go-test/cmd/gotest@latest
    - gotest summary ./... -race -coverprofile=coverage.out
    - go tool cover -html=coverage.out -o coverage.html
  artifacts:
    paths:
      - coverage.html

You can also pipe existing go test -json output into gotest summary without re-running tests:

go test -json ./... | gotest summary --input=-

This is useful if you have existing CI steps that run go test and you want to add summary output without changing the test execution.

Combining stdlib and suite tests

Many projects have a mix of standard func Test* tests and gotest suites. The recommended CI pattern runs both:

.github/workflows/test.yml
steps:
  - uses: actions/checkout@v4
  - uses: actions/setup-go@v5
    with:
      go-version-file: go.mod

  # Standard tests (go test directly)
  - name: Test (stdlib)
    run: go test -coverprofile=coverage-stdlib.out ./... -race

  # Suite tests (gotest)
  - uses: mvrahden/go-test@v1
    with:
      packages: ./...
      race: true
      coverage: true
      min-coverage: 80

go test runs standard test functions. gotest runs suite tests — stdlib func Test* functions are reported but not executed by gotest, so nothing runs twice. Each step enforces its own coverage: the stdlib step writes coverage-stdlib.out, and the gotest step reports its percentage through the action’s coverage output. The gotest step’s CI mode (auto-detected via the CI environment variable) catches committed focus prefixes and locks snapshots.

Project configuration with .gotest.yml

Instead of passing flags in every CI step, you can commit a .gotest.yml to your project root. gotest reads it automatically in both local development and CI:

.gotest.yml
min-coverage: 80
parallel: 12
tags: integration
lint:
  skip:
    - testify

CLI flags override .gotest.yml, so a one-off gotest --min=90 ./... can tighten the gate for a single run. (A --min of zero is treated as unset, so it does not disable a configured threshold.) The default, both locally and in CI, is whatever the project file says.

A complete workflow

A complete CI setup for a project using gotest looks like this:

  1. Add gotest to go.mod via a tool directive or go get.
  2. Create .gotest.yml with your coverage threshold and any project-wide settings.
  3. Add the GitHub Action to your workflow. One step, four inputs.
  4. Push. CI mode activates automatically. Focus prefixes are caught, snapshots are read-only, failures get annotations, and coverage is enforced.

One developer sets this up. The entire team benefits. The coverage threshold prevents regression, the focus guard prevents accidental test skipping, and the failure summary makes every CI failure actionable without scrolling through logs. From here, the inner loop post covers the local workflow that CI mode guards, and Your First Go Test Suite in 10 Minutes is the place to send teammates who are new to gotest.

Set up gotest in your CI pipeline today.

go install github.com/mvrahden/go-test/cmd/gotest@latest