Introduction
This guide will help you integrate OtterWise with your Go project to track code coverage. OtterWise now natively supports Go's coverage output format, making setup incredibly simple - no conversion tools needed!
Whether you're building web services, CLI tools, microservices, or libraries, this guide covers the setup for your specific use case.
📚 Looking for best practices?
Check out our comprehensive guide: Go Code Coverage Tracking: Best Practices and CI/CD Integration
Native Go Coverage Support
OtterWise directly parses Go's coverage profile format. Simply run go test -coverprofile=coverage.out and upload the file - that's it!
Prerequisites
Before setting up OtterWise for your Go project, you'll need:
- A GitHub repository connected to OtterWise
- Go installed (version 1.19 or higher recommended)
- Tests written for your Go code
- Your OtterWise repository token (found in your repository settings after enabling it in OtterWise)
Supported Coverage Formats
OtterWise supports the following coverage formats for Go projects:
- Go Coverage Profile (Native) - Go's built-in coverage format (
coverage.out,cover.out) ← Recommended - Cobertura XML - Converted from Go coverage using tools like gocover-cobertura
- Clover XML - Alternative XML format (less common but supported)
We recommend using Go's native coverage format as it requires no additional tools and is fully supported by OtterWise.
Basic Setup with Go's Native Coverage
Go's built-in testing framework includes comprehensive coverage support. This is the simplest and recommended approach.
Running Tests with Coverage
Generate coverage data for your entire project:
go test -coverprofile=coverage.out ./...
This command:
- Runs all tests in your project
- Generates a coverage profile at
coverage.out - Works with all Go test frameworks (standard library, Testify, Ginkgo, etc.)
Coverage Modes
Go supports three coverage modes. Choose based on your needs:
Set Mode (Default):go test -coverprofile=coverage.out -covermode=set ./...
Tracks which statements were executed (1) or not (0). Fastest option.
Count Mode:go test -coverprofile=coverage.out -covermode=count ./...
Counts how many times each statement was executed. Useful for identifying hot paths.
Atomic Mode:go test -coverprofile=coverage.out -covermode=atomic ./...
Like count mode but thread-safe. Use for parallel tests.
Uploading to OtterWise
After generating the coverage profile, upload it to OtterWise using the bash uploader:
# Using environment variable export OTTERWISE_TOKEN=your-repo-token bash <(curl -s https://raw.githubusercontent.com/getOtterWise/bash-uploader/main/uploader.sh) # Or pass token directly bash <(curl -s https://raw.githubusercontent.com/getOtterWise/bash-uploader/main/uploader.sh) --repo-token your-repo-token
Testing Frameworks
Standard Library (testing)
Go's standard library testing package provides everything you need for basic testing and coverage.
Example test file (calculator_test.go):
package calculator
import "testing"
func TestAdd(t *testing.T) {
result := Add(2, 3)
if result != 5 {
t.Errorf("Add(2, 3) = %d; want 5", result)
}
}
func TestSubtract(t *testing.T) {
tests := []struct {
name string
a, b int
want int
}{
{"positive", 5, 3, 2},
{"negative", 3, 5, -2},
{"zero", 5, 5, 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := Subtract(tt.a, tt.b)
if got != tt.want {
t.Errorf("Subtract(%d, %d) = %d; want %d",
tt.a, tt.b, got, tt.want)
}
})
}
}
Run with coverage:
go test -coverprofile=coverage.out ./...
Testify
Testify is the most popular Go testing framework, providing assertions, mocks, and test suites.
Installation:
go get github.com/stretchr/testify
Example with assertions:
package calculator
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestAdd(t *testing.T) {
assert.Equal(t, 5, Add(2, 3), "they should be equal")
}
func TestSubtract(t *testing.T) {
assert := assert.New(t)
assert.Equal(2, Subtract(5, 3))
assert.Equal(-2, Subtract(3, 5))
assert.Equal(0, Subtract(5, 5))
}
Example with mocks:
package service
import (
"testing"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/assert"
)
// Mock object
type MockDatabase struct {
mock.Mock
}
func (m *MockDatabase) GetUser(id int) (string, error) {
args := m.Called(id)
return args.String(0), args.Error(1)
}
func TestUserService(t *testing.T) {
mockDB := new(MockDatabase)
mockDB.On("GetUser", 123).Return("John Doe", nil)
service := NewUserService(mockDB)
name, err := service.GetUserName(123)
assert.NoError(t, err)
assert.Equal(t, "John Doe", name)
mockDB.AssertExpectations(t)
}
Run with coverage (same command):
go test -coverprofile=coverage.out ./...
Ginkgo & Gomega
Ginkgo is a BDD-style testing framework, often paired with Gomega for assertions.
Installation:
go get github.com/onsi/ginkgo/v2/ginkgo go get github.com/onsi/gomega/...
Example test:
package calculator_test
import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"your-package/calculator"
)
var _ = Describe("Calculator", func() {
Describe("Add", func() {
It("should add two numbers correctly", func() {
result := calculator.Add(2, 3)
Expect(result).To(Equal(5))
})
})
Describe("Subtract", func() {
Context("when subtracting positive numbers", func() {
It("should return correct result", func() {
Expect(calculator.Subtract(5, 3)).To(Equal(2))
})
})
})
})
Run with coverage:
ginkgo -r --coverprofile=coverage.out
Framework-Specific Setup
HTTP Services (net/http, Gin, Echo, Fiber)
Testing HTTP handlers and services works the same way - just use httptest for request/response testing.
package main
import (
"net/http"
"net/http/httptest"
"testing"
)
func TestHealthHandler(t *testing.T) {
req := httptest.NewRequest("GET", "/health", nil)
w := httptest.NewRecorder()
HealthHandler(w, req)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d", w.Code)
}
}
Example with Gin:
package main
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
)
func TestPingRoute(t *testing.T) {
gin.SetMode(gin.TestMode)
router := SetupRouter()
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/ping", nil)
router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code)
assert.Equal(t, `{"message":"pong"}`, w.Body.String())
}
Run coverage for any framework:
go test -coverprofile=coverage.out ./...
gRPC Services
Testing gRPC services follows the same pattern - mock your dependencies and test the handlers.
package service
import (
"context"
"testing"
pb "your-package/proto"
"github.com/stretchr/testify/assert"
)
func TestGreeterServer(t *testing.T) {
s := &server{}
req := &pb.HelloRequest{Name: "World"}
resp, err := s.SayHello(context.Background(), req)
assert.NoError(t, err)
assert.Equal(t, "Hello World", resp.Message)
}
CI Integration Examples
GitHub Actions
Here's a complete example for a Go project using GitHub Actions with the official OtterWise action:
name: Tests
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
go-version: ['1.21', '1.22', '1.23']
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
cache: true
- name: Install dependencies
run: go mod download
- name: Run tests with coverage
run: go test -coverprofile=coverage.out -covermode=atomic ./...
- name: Upload Coverage to OtterWise
if: matrix.go-version == '1.23'
uses: getOtterWise/github-action@v1
with:
token: ${{ secrets.OTTERWISE_TOKEN }}
Important
Remember to add your OTTERWISE_TOKEN to GitHub Secrets (Settings > Secrets and variables > Actions).
Note: We only upload coverage from one Go version (1.23 in this example) to avoid duplicate reports.
GitHub Actions with Multiple Packages
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.23'
- name: Install dependencies
run: |
go mod download
go install github.com/golang/mock/mockgen@latest
- name: Generate mocks
run: go generate ./...
- name: Run tests
run: go test -v -race -coverprofile=coverage.out -covermode=atomic ./...
- name: Upload Coverage to OtterWise
uses: getOtterWise/github-action@v1
with:
token: ${{ secrets.OTTERWISE_TOKEN }}
GitLab CI
test:
image: golang:1.23
stage: test
before_script:
- go mod download
script:
- go test -coverprofile=coverage.out -covermode=atomic ./...
- bash <(curl -s https://raw.githubusercontent.com/getOtterWise/bash-uploader/main/uploader.sh) --repo-token $OTTERWISE_TOKEN
coverage: '/coverage: \d+.\d+% of statements/'
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage.out
CircleCI
version: 2.1
jobs:
test:
docker:
- image: cimg/go:1.23
steps:
- checkout
- restore_cache:
keys:
- go-mod-v1-{{ checksum "go.sum" }}
- run:
name: Install Dependencies
command: go mod download
- save_cache:
key: go-mod-v1-{{ checksum "go.sum" }}
paths:
- "/go/pkg/mod"
- run:
name: Run Tests with Coverage
command: go test -coverprofile=coverage.out -covermode=atomic ./...
- run:
name: Upload Coverage to OtterWise
command: bash <(curl -s https://raw.githubusercontent.com/getOtterWise/bash-uploader/main/uploader.sh) --repo-token $OTTERWISE_TOKEN
workflows:
version: 2
test:
jobs:
- test
Using Makefiles for Coverage
Many Go projects use Makefiles to standardize common tasks. Here's a complete example:
.PHONY: test coverage coverage-html clean # Run all tests test: go test -v -race ./... # Run tests with coverage coverage: go test -coverprofile=coverage.out -covermode=atomic ./... # Generate HTML coverage report coverage-html: coverage go tool cover -html=coverage.out -o coverage.html @echo "Coverage report generated: coverage.html" # View coverage in browser coverage-view: coverage-html open coverage.html # macOS # xdg-open coverage.html # Linux # Run tests with coverage and upload to OtterWise coverage-upload: coverage @bash <(curl -s https://raw.githubusercontent.com/getOtterWise/bash-uploader/main/uploader.sh) # Clean generated files clean: rm -f coverage.out coverage.html
Then in your CI:
make coverage
Quick Reference: Coverage Commands
| Use Case | Command | Output |
|---|---|---|
| Basic coverage | go test -coverprofile=coverage.out ./... |
coverage.out |
| With race detection | go test -race -coverprofile=coverage.out ./... |
coverage.out |
| Atomic mode (parallel safe) | go test -covermode=atomic -coverprofile=coverage.out ./... |
coverage.out |
| Specific package | go test -coverprofile=coverage.out ./pkg/mypackage |
coverage.out |
| View coverage in terminal | go tool cover -func=coverage.out |
Terminal output |
| Generate HTML report | go tool cover -html=coverage.out -o coverage.html |
coverage.html |
Advanced Configuration
Excluding Files from Coverage
You can exclude specific files or directories using build tags or by organizing your code structure.
Using build tags:// +build !test package main // This file won't be included in test buildsExclude test utilities:
Files ending in _test.go are automatically excluded from coverage.
Put test utilities in these files to exclude them from coverage calculations.
Testing Multiple Packages
Test all packages in your module with a single command:
# All packages go test -coverprofile=coverage.out ./... # Specific subdirectories go test -coverprofile=coverage.out ./cmd/... ./pkg/... # With verbose output go test -v -coverprofile=coverage.out ./...
Parallel Test Execution
Speed up test execution by running tests in parallel:
# Run tests in parallel (use atomic mode for accuracy)
go test -parallel 4 -coverprofile=coverage.out -covermode=atomic ./...
# Set parallelism in individual tests
func TestSomething(t *testing.T) {
t.Parallel()
// test code
}
Troubleshooting
Common Issues and Solutions
| Issue | Solution |
|---|---|
| Coverage file not found |
|
| Tests pass but no coverage |
|
| Import cycle errors |
|
| Race detector failures |
|
| Coverage differs locally vs CI |
|
Debugging Coverage Generation
To verify your coverage is being generated correctly:
# Run coverage locally go test -coverprofile=coverage.out ./... # Check if the file exists ls -la coverage.out # View coverage summary go tool cover -func=coverage.out # View the first few lines of the coverage file head -20 coverage.out
A valid Go coverage file should start like this:
mode: set github.com/yourusername/yourproject/pkg/calculator/calculator.go:10.13,12.2 1 1 github.com/yourusername/yourproject/pkg/calculator/calculator.go:14.16,16.2 1 1 github.com/yourusername/yourproject/pkg/calculator/calculator.go:18.16,20.2 1 0
Converting to XML (Optional)
While OtterWise natively supports Go coverage files, you may want to convert to XML for other tools.
Using gocover-cobertura
# Install go install github.com/boumenot/gocover-cobertura@latest # Generate coverage and convert go test -coverprofile=coverage.out ./... gocover-cobertura < coverage.out > coverage.xml
Using gocov + gocov-xml
# Install go install github.com/axw/gocov/gocov@latest go install github.com/AlekSi/gocov-xml@latest # Generate coverage and convert go test -coverprofile=coverage.out ./... gocov convert coverage.out | gocov-xml > coverage.xml
Additional Upload Options
The OtterWise bash uploader supports several options for advanced use cases. See the Bash Uploader documentation for complete details.
Common options for Go projects:
# Specify custom coverage file location bash <(curl -s https://raw.githubusercontent.com/getOtterWise/bash-uploader/main/uploader.sh) \ --repo-token $OTTERWISE_TOKEN \ --file ./coverage.out # Add a flag for test suite identification bash <(curl -s https://raw.githubusercontent.com/getOtterWise/bash-uploader/main/uploader.sh) \ --repo-token $OTTERWISE_TOKEN \ --flag unit-tests # For monorepos - track different packages separately bash <(curl -s https://raw.githubusercontent.com/getOtterWise/bash-uploader/main/uploader.sh) \ --repo-token $OTTERWISE_TOKEN \ --file ./services/api/coverage.out \ --flag api-service
Next Steps
Once you have coverage data uploading successfully, you can:
- Configure Pull Request comments to see coverage changes in your PRs
- Set up status checks to enforce coverage thresholds
- Enable line annotations to highlight uncovered code in PRs
- Use badges to display your coverage status in your README
For additional help, check our example repository or contact OtterWise support.