Go Setup Guide

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.

Example with net/http:
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 builds
Exclude 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
  • Verify tests run successfully: go test ./...
  • Check current directory: ls -la coverage.out
  • Ensure you're using -coverprofile flag
Tests pass but no coverage
  • Make sure you have actual test functions (start with Test)
  • Check that test files end with _test.go
  • Verify package names match between code and tests
Import cycle errors
  • Use a separate test package: package mypackage_test
  • Restructure imports to break cycles
  • Consider using interfaces to decouple dependencies
Race detector failures
  • Fix race conditions before measuring coverage
  • Use -covermode=atomic for concurrent code
  • Add proper synchronization (mutexes, channels)
Coverage differs locally vs CI
  • Ensure same Go version in both environments
  • Check for OS-specific test files (_darwin.go, _linux.go)
  • Verify all dependencies are available in CI
  • Check for environment-specific build tags

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:

For additional help, check our example repository or contact OtterWise support.