Photo by Jorge Salvador on Unsplash

Notes about go coverage

Vahid Mostofi
3 min readOct 18, 2023

--

Since version 1.2, Go has supported getting coverage at the package level if cover is provided to the go test command. Go 1.20 introduced coverage-instrumented programs. You can read about that in the blog post on the official Go website Code coverage for Go integration tests.

It basically comes down to:

  1. When compiling (or building), pass cover.
  2. When running the program, set the GOCOVERDIR.
  3. Use go tool covdata percent -i=<value of GOCOVERDIR>.

GOCOVERDIR stores the location on the file system where Go stores the coverage information.

While using this for a special case, I noticed a few notes that I’m adding there. I’ll keep adding more as new ones arise.

go test -cover changes the GOCOVERDIR

When running go test -cover, the Go test programmer changes the GOCOVERDIR environment variable and uses it to gather the coverage of the tests and store them somewhere on the filesystem to report the coverage at the end.

For example, if I have a function that I’m testing like this:

func GetFirstValue() (int, error) {
return 1, nil
}
// --------------------------------------
func TestGetFirstValue(t *testing.T) {
t.Logf("Testing GetFirstValue start: %s\\n", os.Getenv("GOCOVERDIR"))
t.Cleanup(func() {
t.Logf("Testing GetFirstValue end: %s\\n", os.Getenv("GOCOVERDIR"))
})
}

and add a wrapper for running tests like wrap_tests_for_coverage.sh such as:

#!/bin/bash

export GOCOVERDIR=covdatafiles
echo $GOCOVERDIR
go test ./... -v -coverprofile=coverage.out
echo $GOCOVERDIR

Running ./wrap_tests_for_coverage.sh results in:

covdatafiles 
=== RUN TestGetFirstValue
main_test.go:9: Testing GetFirstValue start: /var/folders/jk/08m726xj1kz4yvsrxmv3_c6r0000gn/T/go-build3127326276/b001/gocoverdir
main_test.go:11: Testing GetFirstValue end: /var/folders/jk/08m726xj1kz4yvsrxmv3_c6r0000gn/T/go-build3127326276/b001/gocoverdir
--- PASS: TestGetFirstValue (0.00s)
PASS
coverage: 0.0% of statements
ok github.com/vahidmostofi/learn-go/coverage 0.256s coverage: 0.0% of statements
covdatafiles

Note that the value of $GOCOVERDIR before and after the tests is the same, but it is different from what we have while running the tests. This is because the go test command actually uses this environment variable to gather coverage while running the test.

No coverage if the code panics.

I also noted that if the program that is compiled with -cover panics, the coverage won’t be reported. This actually surprised me a bit as I expected the -cover results in a system that go runtime writes the coverage down and then exists. Let’s say we have a simple code like this:

package main

import (
"errors"
)

var ErrGettingSecondValue = errors.New("error getting second value")
func main() {
firstValue, err := GetFirstValue()
if err != nil {
panic(err)
}
secondValue, err := GetSecondValue()
if err != nil {
panic("Error getting the second value")
}
println(firstValue + secondValue)
}

func GetFirstValue() (int, error) {
return 1, nil
}

func GetSecondValue() (int, error) {
return 0, ErrGettingSecondValue
}

With tests like this:

package main

import (
"testing"
)

func TestGetFirstValue(t *testing.T) {
v, err := GetFirstValue()
if v != 1 {
t.FailNow()
}
if err != nil {
t.FailNow()
}
}

func TestGetSecondValue(t *testing.T) {
v, err := GetSecondValue()
if v != 1 {
t.FailNow()
}
if err != nil {
t.FailNow()
}
}

and a piece of script for running the program and showing the coverage (wrap_main_for_coverage.sh) like this:

#!/bin/sh

rm -rf covdatafiles
mkdir covdatafiles
go build -cover -o coverage ./...
GOCOVERDIR=covdatafiles ./coverage
go tool covdata percent -i=covdatafiles
go tool covdata textfmt -i ./covdatafiles -o coverage-from-running.out
go tool cover -html=coverage-from-running.out

The coverage output would be like this:

Note that we are not even getting coverage for lines that were executed before the panic happened.

But as soon as we add a recover at the beginning of the main function and run the tests, we obtain the coverage information.

...
func main(){
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered from panic in main:", r)
}
}()
...
}

Running wrap_main_for_coverage.sh now results in:

--

--

Vahid Mostofi

Senior Software Engineer with focus on backe-end and devops