Question

How to Measure Test Coverage in Go

gounit-testingtestingcode-coverage

Question

I want to measure code coverage for Go unit tests. Is there a standard way to generate a coverage report in Go, and what tools or commands should I use?

Short Answer

By the end of this page, you will understand how test coverage works in Go, how to generate coverage reports with Go's built-in tools, how to read the results, and how coverage fits into real testing workflows.

Concept

Go includes built-in support for test coverage, so you usually do not need a separate third-party tool just to get started. Test coverage tells you which parts of your code were executed while your tests ran.

In Go, coverage is most commonly measured with the go test command and the -cover flags. This helps you answer questions like:

  • Did my tests actually execute this function?
  • Which files have low coverage?
  • Are there branches or cases I forgot to test?

Coverage matters because it gives feedback about how much of your code your tests touch. That said, coverage is a signal, not proof of correctness. A test can execute a line without properly checking the result.

Go's coverage tooling is practical because it is:

  • Built in to the language toolchain
  • Easy to run locally
  • Useful in CI pipelines
  • Good for spotting untested areas quickly

The most common outputs are:

  • A percentage shown in the terminal
  • A coverage profile file
  • An HTML report you can inspect visually

A simple workflow looks like this:

  1. Write unit tests in *_test.go files
  2. Run go test -cover
  3. Generate a coverage profile with -coverprofile
  4. View details with go tool cover

This concept is important in real programming because teams often use coverage reports to maintain testing standards, catch untested code paths, and track quality over time.

Mental Model

Think of test coverage like walking through rooms in a building with a motion sensor.

  • Your codebase is the building
  • Each function or line is a room
  • Your tests are the person walking through the building
  • Coverage shows which rooms were entered

If a room was never entered, that part of the code was not tested at all. But entering a room does not mean you checked whether everything inside it works correctly. That is why coverage is useful, but not enough by itself.

Take Quiz

Syntax and Examples

The basic Go command for coverage is:

go test -cover

This runs tests in the current package and prints a coverage percentage.

Example code

package mathutil

func Add(a, b int) int {
	return a + b
}

func IsEven(n int) bool {
	return n%2 == 0
}
package mathutil

import "testing"

func TestAdd(t *testing.T) {
	got := Add(2, 3)
	want := 5

	if got != want {
		t.Errorf("Add(2, 3) = %d; want %d", got, want)
	}
}

Run:

go test -cover

Possible output:

PASS
coverage: 50.0% of statements
ok   example.com/mathutil  0.003s

This happened because Add was tested, but IsEven was never executed.

Generate a coverage profile

go test -coverprofile=coverage.out

This writes detailed coverage data to coverage.out.

View coverage by function

go tool cover -func=coverage.out

Example output:

example.com/mathutil/mathutil.go:3:  Add     100.0%
example.com/mathutil/mathutil.go:7:  IsEven  0.0%
total:                                (statements) 50.0%

Open an HTML coverage report

go tool cover -html=coverage.out

This opens a visual report where covered lines and uncovered lines are highlighted.

Test all packages

go test ./... -cover

This is common in real projects because it runs tests across the whole module.

Step by Step Execution

Consider this code:

package discount

func PriceAfterDiscount(price int, discount int) int {
	if discount <= 0 {
		return price
	}

	return price - discount
}

And this test:

package discount

import "testing"

func TestPriceAfterDiscount(t *testing.T) {
	got := PriceAfterDiscount(100, 20)
	want := 80

	if got != want {
		t.Fatalf("got %d, want %d", got, want)
	}
}

Now run:

go test -coverprofile=coverage.out

What happens step by step

  1. Go finds all test files ending in *_test.go.
  2. It builds an instrumented version of your code.
    • Instrumented means Go adds tracking so it can record which statements run.
  3. Go runs TestPriceAfterDiscount.
  4. The test calls PriceAfterDiscount(100, 20).
  5. Inside the function:
    • discount <= 0 is checked
    • The condition is false
    • The function returns price - discount, which is 80
  6. The test compares got and want.
  7. Because they match, the test passes.
  8. Coverage data is written to coverage.out.

Important observation

The if discount <= 0 line was executed, but only the false path was used. If you never test discount <= 0, your coverage may miss an important scenario.

Add another test:

func TestPriceAfterDiscount_NoDiscount(t *testing.T) {
	got := PriceAfterDiscount(100, 0)
	want := 100

	if got != want {
		t.Fatalf("got %d, want %d", got, want)
	}
}

Now more of the function's behavior is exercised. This is a good example of using coverage to discover missing test cases.

Real World Use Cases

Coverage reporting in Go is useful in many practical situations:

API handlers

You may want to confirm that tests execute:

  • Success responses
  • Validation failures
  • Unauthorized access
  • Internal error paths

Business logic

For pricing, permissions, or calculations, coverage helps reveal missing edge-case tests.

Data processing scripts

If a Go program parses files or transforms records, coverage can show whether tests hit:

  • Empty input
  • Invalid rows
  • Normal valid input
  • Large batches

Libraries and packages

If you publish a reusable Go package, coverage helps you see whether your public functions are actually tested.

CI pipelines

Teams often run commands like:

go test ./... -coverprofile=coverage.out

Then they may:

  • Store the report as a build artifact
  • Fail the build if coverage drops too far
  • Track coverage trends over time

Refactoring safety

Before changing older code, developers often review coverage to understand whether enough tests exist to make refactoring safer.

Take Quiz

Real Codebase Usage

In real Go projects, coverage is usually part of a broader testing workflow rather than a one-off command.

Common patterns developers use

Run coverage across the whole module

go test ./... -cover

This is common in monorepos and multi-package applications.

Generate a reusable profile in CI

go test ./... -coverprofile=coverage.out

The profile can then be analyzed or uploaded to reporting systems.

Inspect function-level gaps

go tool cover -func=coverage.out

Developers use this to identify exactly which functions are under-tested.

Use HTML reports during debugging

go tool cover -html=coverage.out

This helps visually inspect untested lines after writing or refactoring tests.

How coverage supports code patterns

Guard clauses and early returns

Functions often return early on invalid input. Coverage helps confirm that tests hit both:

  • The early return path
  • The normal path

Validation logic

If a function checks for missing fields, bad formats, or invalid ranges, coverage can reveal whether error cases are tested.

Error handling

Real applications should test both success and failure paths. Coverage makes failure paths easier to notice.

Table-driven tests

Go developers commonly write table-driven tests to increase coverage cleanly.

func TestIsEven(t *testing.T) {
	tests := []struct {
		input int
		want  bool
	}{
		{2, true},
		{3, false},
		{0, true},
		{-4, true},
	}

	for _, tt := range tests {
		got := IsEven(tt.input)
		if got != tt.want {
			t.Errorf("IsEven(%d) = %v; want %v", tt.input, got, tt.want)
		}
	}
}

This pattern is widely used because it covers multiple cases without repeating test structure.

Practical reminder

A codebase with high coverage can still have weak tests. Strong tests should:

  • Check real outputs
  • Include edge cases
  • Verify errors when appropriate
  • Avoid only testing the happy path

Common Mistakes

1. Thinking coverage means correctness

A high percentage does not guarantee your code is bug-free.

Broken idea:

func TestAdd(t *testing.T) {
	Add(2, 3)
}

This may execute the function, but it does not assert anything meaningful.

Better:

func TestAdd(t *testing.T) {
	got := Add(2, 3)
	want := 5
	if got != want {
		t.Fatalf("got %d, want %d", got, want)
	}
}

2. Only running coverage in one package

If you run:

go test -cover

it only covers the current package. Beginners often expect the whole project to be included.

Use this for all packages:

go test ./... -cover

3. Ignoring edge cases

A function may appear covered even though important branches are missing.

Example:

func Divide(a, b int) int {
	if b == 0 {
		return 0
	}
	return a / b
}

If you only test Divide(10, 2), the zero-divisor case is still untested.

4. Confusing statement coverage with full logic coverage

Go's default coverage focuses on executed statements. That is helpful, but it does not automatically mean every logical combination was tested.

5. Forgetting to generate a profile before using go tool cover

This will not work correctly without a coverage file:

go tool cover -func=coverage.out

First create the file:

go test -coverprofile=coverage.out

6. Chasing 100% coverage blindly

Trying to force 100% coverage can waste time or encourage meaningless tests. Focus on:

  • Critical business logic
  • Error paths
  • Edge cases
  • Regressions that have happened before

Comparisons

ApproachWhat it doesBest forNotes
go test -coverShows a quick coverage percentageFast local checksEasy starting point
go test -coverprofile=coverage.outSaves detailed coverage dataCI and deeper analysisNeeded for reports
go tool cover -func=coverage.outShows coverage by functionFinding weak spotsGood for review
go tool cover -html=coverage.outOpens a visual reportExploring uncovered linesVery beginner-friendly

Coverage vs test quality

ConceptMeaning
CoverageHow much code was executed during tests
Test qualityHow well tests verify correct behavior

You want both.

Current package vs all packages

CommandScope
go test -coverCurrent package only
go test ./... -coverAll packages in the module

Quick check vs detailed analysis

NeedCommand
Just see a percentagego test -cover
Save report for latergo test -coverprofile=coverage.out
Inspect per-function resultsgo tool cover -func=coverage.out
View highlighted source codego tool cover -html=coverage.out
Take Quiz

Cheat Sheet

# Run tests in current package with coverage
go test -cover

# Run tests in all packages with coverage
go test ./... -cover

# Generate a coverage profile
go test ./... -coverprofile=coverage.out

# Show per-function coverage
go tool cover -func=coverage.out

# Open HTML coverage report
go tool cover -html=coverage.out

Key points

  • Go has built-in coverage tooling.
  • Test files should end with *_test.go.
  • Coverage shows executed statements during tests.
  • Coverage is useful, but it does not prove correctness.
  • Use ./... to test all packages in a module.
  • Use -coverprofile when you want detailed reporting.

Good habits

  • Test both happy paths and error paths.
  • Add assertions, not just function calls.
  • Use table-driven tests for multiple cases.
  • Review uncovered lines after refactoring.

Common command sequence

go test ./... -coverprofile=coverage.out
go tool cover -func=coverage.out
go tool cover -html=coverage.out

FAQ

Does Go have built-in code coverage tools?

Yes. Go includes coverage support through go test and go tool cover.

How do I generate a coverage report in Go?

Run:

go test -coverprofile=coverage.out

Then inspect it with:

go tool cover -func=coverage.out

or:

go tool cover -html=coverage.out

How do I measure coverage for all packages in a Go module?

Use:

go test ./... -cover

or generate a profile with:

go test ./... -coverprofile=coverage.out

What does coverage percentage mean in Go?

It means the percentage of statements that were executed while tests ran.

Is 100% test coverage necessary?

No. High coverage can be helpful, but meaningful assertions and good edge-case testing matter more.

Why does my coverage look low even though tests pass?

Your tests may only exercise a small part of the code, or they may miss branches, error paths, or other packages.

Can I see which lines are not covered?

Yes. Generate a profile and run:

go tool cover -html=coverage.out

This gives you a visual report with highlighted lines.

Take Quiz

Mini Project

Description

Build a small Go package that calculates shipping costs and measure how much of it is covered by tests. This project demonstrates how coverage exposes missing cases and helps you improve your test suite.

Goal

Create a Go package with unit tests, generate a coverage profile, and identify covered and uncovered logic.

Requirements

  1. Create a Go package with one function that contains at least one conditional branch.
  2. Write unit tests for the normal case and at least one edge case.
  3. Run coverage with -coverprofile.
  4. Inspect the report using go tool cover -func or go tool cover -html.
  5. Update tests if you discover an uncovered path.
Take Quiz

Keep learning

Related questions

View more