Go Coverage
Introduction
Code coverage is a metric that helps you understand how much of your Go code is being tested by your test suite. It identifies which lines of code are executed during your tests and which are not. By measuring code coverage, you can identify untested code, potential bugs, and improve the overall quality of your application.
In this guide, we'll explore Go's built-in coverage tools, how to generate and interpret coverage reports, and best practices for improving your test coverage.
Understanding Code Coverage in Go
Go provides native support for code coverage through the go test
command with the -cover
flag. This built-in functionality makes it easy to start measuring how thoroughly your code is tested without needing external tools.
What Code Coverage Measures
Coverage analysis in Go tracks:
- Statement coverage: Which lines of code are executed during tests
- Branch coverage: Whether conditional branches (if/else, switch cases) are tested
- Function coverage: Which functions are called during test execution
Why Code Coverage Matters
- Identifies untested parts of your codebase
- Helps prevent bugs and regressions
- Encourages writing testable code
- Provides confidence in your test suite
- Serves as a quality metric for your project
Basic Coverage Testing
Let's start with a simple example to demonstrate how Go's coverage tools work.
Example: A Simple Calculator Package
First, let's create a simple calculator package:
// calculator/calculator.go
package calculator
// Add returns the sum of two integers
func Add(a, b int) int {
return a + b
}
// Subtract returns the difference between two integers
func Subtract(a, b int) int {
return a - b
}
// Multiply returns the product of two integers
func Multiply(a, b int) int {
return a * b
}
// Divide returns the quotient of two integers
// If b is 0, it returns 0 to avoid division by zero
func Divide(a, b int) int {
if b == 0 {
return 0
}
return a / b
}
Now let's write a test file that only tests some of the functions:
// calculator/calculator_test.go
package calculator
import "testing"
func TestAdd(t *testing.T) {
sum := Add(2, 3)
if sum != 5 {
t.Errorf("Expected Add(2, 3) = 5, got %d", sum)
}
}
func TestSubtract(t *testing.T) {
difference := Subtract(5, 3)
if difference != 2 {
t.Errorf("Expected Subtract(5, 3) = 2, got %d", difference)
}
}
Running Basic Coverage Test
To see the basic coverage information, run:
go test -cover ./calculator
Output:
ok example.com/calculator 0.004s coverage: 50.0% of statements
This tells us that our tests are covering only 50% of the statements in the package. We've written tests for Add
and Subtract
, but not for Multiply
and Divide
.
Generating Coverage Profiles
For a more detailed analysis, we can generate a coverage profile and use Go's tools to examine it.
go test -coverprofile=coverage.out ./calculator
This creates a file called coverage.out
containing detailed coverage information. We can view this information in different formats.
Viewing Coverage in the Terminal
To see a line-by-line breakdown of coverage in the terminal:
go tool cover -func=coverage.out
Output:
example.com/calculator/calculator.go:4: Add 100.0%
example.com/calculator/calculator.go:9: Subtract 100.0%
example.com/calculator/calculator.go:14: Multiply 0.0%
example.com/calculator/calculator.go:19: Divide 0.0%
total: (statements) 50.0%
This shows that Add
and Subtract
are fully covered, while Multiply
and Divide
are not covered at all.
Generating HTML Coverage Report
For a more visual representation, we can generate an HTML report:
go tool cover -html=coverage.out -o coverage.html
This creates an HTML file that color-codes your source code:
- Green: Code that is covered by tests
- Red: Code that is not covered by tests
Improving Code Coverage
Let's improve our code coverage by adding tests for the remaining functions:
// Adding to calculator/calculator_test.go
func TestMultiply(t *testing.T) {
product := Multiply(2, 3)
if product != 6 {
t.Errorf("Expected Multiply(2, 3) = 6, got %d", product)
}
}
func TestDivide(t *testing.T) {
// Test normal division
quotient := Divide(6, 3)
if quotient != 2 {
t.Errorf("Expected Divide(6, 3) = 2, got %d", quotient)
}
// Test division by zero
quotient = Divide(6, 0)
if quotient != 0 {
t.Errorf("Expected Divide(6, 0) = 0, got %d", quotient)
}
}
Running the coverage test again:
go test -cover ./calculator
Output:
ok example.com/calculator 0.004s coverage: 100.0% of statements
Now we have 100% code coverage! Every line of our code is being executed during tests.
Coverage in Real-World Applications
In real-world applications, achieving 100% coverage is often challenging and may not always be necessary. Let's look at a more practical example.
Example: HTTP Server
Here's a simple HTTP server with an endpoint that requires more comprehensive testing:
// server/server.go
package server
import (
"encoding/json"
"net/http"
)
type Response struct {
Message string `json:"message"`
Status int `json:"status"`
}
func HelloHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
w.WriteHeader(http.StatusMethodNotAllowed)
json.NewEncoder(w).Encode(Response{
Message: "Method not allowed",
Status: http.StatusMethodNotAllowed,
})
return
}
name := r.URL.Query().Get("name")
if name == "" {
name = "World"
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(Response{
Message: "Hello, " + name + "!",
Status: http.StatusOK,
})
}
Here's how we could test this handler with good coverage:
// server/server_test.go
package server
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
)
func TestHelloHandler(t *testing.T) {
// Test case 1: GET request without name parameter
req, err := http.NewRequest("GET", "/hello", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
handler := http.HandlerFunc(HelloHandler)
handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v",
status, http.StatusOK)
}
var response Response
err = json.Unmarshal(rr.Body.Bytes(), &response)
if err != nil {
t.Fatal(err)
}
expected := "Hello, World!"
if response.Message != expected {
t.Errorf("handler returned unexpected message: got %v want %v",
response.Message, expected)
}
// Test case 2: GET request with name parameter
req, err = http.NewRequest("GET", "/hello?name=Go", nil)
if err != nil {
t.Fatal(err)
}
rr = httptest.NewRecorder()
handler.ServeHTTP(rr, req)
err = json.Unmarshal(rr.Body.Bytes(), &response)
if err != nil {
t.Fatal(err)
}
expected = "Hello, Go!"
if response.Message != expected {
t.Errorf("handler returned unexpected message: got %v want %v",
response.Message, expected)
}
// Test case 3: POST request (method not allowed)
req, err = http.NewRequest("POST", "/hello", nil)
if err != nil {
t.Fatal(err)
}
rr = httptest.NewRecorder()
handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusMethodNotAllowed {
t.Errorf("handler returned wrong status code: got %v want %v",
status, http.StatusMethodNotAllowed)
}
}
When we run the coverage test for this package:
go test -cover ./server
Output:
ok example.com/server 0.005s coverage: 100.0% of statements
We've achieved 100% coverage by testing all the different paths through our handler.
Advanced Coverage Features
Setting Coverage Thresholds
In CI/CD pipelines, it's common to enforce minimum coverage thresholds. You can write a simple script to fail the build if coverage falls below a certain percentage:
#!/bin/bash
go test -cover ./... | grep -v "no test files" > coverage.txt
COVERAGE=$(grep -o "[0-9\.]*%" coverage.txt | grep -o "[0-9\.]*" | awk '{s+=$1}END{print s/NR}')
THRESHOLD=80
echo "Coverage: $COVERAGE%"
if (( $(echo "$COVERAGE < $THRESHOLD" | bc -l) )); then
echo "Code coverage is below threshold of $THRESHOLD%"
exit 1
fi
Generating Coverage for Multiple Packages
To generate coverage across multiple packages, use:
go test -coverprofile=coverage.out ./...
This will run tests for all packages in your module and generate a combined coverage profile.
Understanding Coverage Modes
Go offers three different coverage modes:
go test -covermode=set -coverprofile=coverage.out ./... # Default: reports whether each statement was executed
go test -covermode=count -coverprofile=coverage.out ./... # Counts how many times each statement was executed
go test -covermode=atomic -coverprofile=coverage.out ./... # Like count, but safe for concurrent tests
- set: Records whether statements were executed at least once (fastest)
- count: Records how many times each statement was executed
- atomic: Like count, but safe for concurrent tests
For most purposes, the default set
mode is sufficient. Use count
or atomic
when you want to identify hot spots in your code.
Best Practices for Code Coverage
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)