Backend Development 21 min read

Comprehensive Guide to Go Unit Testing: Tools, Mocking, and Dependency Management

This guide explains Go’s built‑in testing framework, assertion libraries, table‑driven and sub‑tests, and demonstrates how to mock functions, structs, interfaces, databases, and Redis using tools such as ngmock, gomock, sqlmock and miniredis, while covering test setup, teardown, coverage handling, and best‑practice insights.

Tencent Cloud Developer
Tencent Cloud Developer
Tencent Cloud Developer
Comprehensive Guide to Go Unit Testing: Tools, Mocking, and Dependency Management

This article introduces the importance of unit testing in Go projects and shares practical experiences from a Tencent backend engineer. It covers testing frameworks, mocking techniques, and handling common dependency problems to make tests maintainable and reliable.

Go test framework : Go’s built‑in go test runs test files ending with _test.go . Test functions must start with Test and accept a *testing.T parameter.

// sample_test.go

package sample_test

import (
    "testing"
)

func TestDownload(t *testing.T) {}
func TestUpload(t *testing.T) {}

The testing.T type provides methods such as t.Fatal (immediate failure), t.Error (record error and continue), and t.Log (debug output when -v is used).

Assertion libraries : The github.com/stretchr/testify/assert package simplifies checks, e.g., assert.Nil(t, err) . The goconvey framework adds BDD‑style assertions.

if err != nil {
    t.Errorf("got error %v", err)
}

Table‑driven tests allow multiple input cases to be defined in a slice, improving coverage of different branches.

func Test_twice(t *testing.T) {
    type args struct { i interface{} }
    tests := []struct {
        name    string
        args    args
        want    int
        wantErr bool
    }{
        {"int", args{i: 10}, 20, false},
        {"string success", args{i: "11"}, 22, false},
        {"string failed", args{i: "aaa"}, 0, true},
        {"unknown type", args{i: []byte("1")}, 0, true},
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got, err := twice(tt.args.i)
            if (err != nil) != tt.wantErr {
                t.Errorf("twice() error = %v, wantErr %v", err, tt.wantErr)
                return
            }
            if got != tt.want {
                t.Errorf("twice() got = %v, want %v", got, tt.want)
            }
        })
    }
}

Sub‑tests with t.Run enable grouping related cases within a single test function.

Mocking dependencies can be done in two ways:

Replace real dependencies with mocks (using libraries such as ngmock or gomock ).

Start a local simulated service (e.g., an HTTP server, Redis instance).

Function mocking with ngmock :

dbNewMock := ngmock.MockFunc(t, db.New)
defer dbNewMock.Finish()

// configure the mock to return an error for any argument
dbNewMock.Mock(ngmock.Any()).Return(nil, errors.New("fake err")).AnyTimes()

Struct method mocking (ngmock):

execCmdMock := ngmock.MockStruct(t, exec.Cmd{}, ngmock.Silent)
defer execCmdMock.Finish()

Mock modes:

Silent : Unmocked methods return zero values.

KeepOrigin : Unmocked methods keep their original behavior.

Mocking a specific method:

execCmdMock.Mock("Output").Return([]byte("1"), nil)

Interface mocking with gomock :

// interfaces.go
type Encoder interface {
    Encode(obj interface{}, w io.Writer) error
}
// generate mock: mockgen -destination=./mockdata/interfaces_mock.go -package=mockdata -source=interfaces.go

ctrl := gomock.NewController(t)
defer ctrl.Finish()
encoderMock := mockdata.NewMockEncoder(ctrl)

encoderMock.EXPECT().Encode(gomock.Any(), gomock.Any()).DoAndReturn(func(obj interface{}, w io.Writer) error {
    w.Write([]byte("test_data"))
    return nil
})

Database mocking with sqlmock :

sqlDB, dbMock, err := sqlmock.New()
// set expectations
dbMock.ExpectBegin()
dbMock.ExpectQuery("select .* from test").WillReturnRows(sqlmock.NewRows([]string{"id", "num"}).AddRow(1, 10).AddRow(2, 20))
dbMock.ExpectExec("update test").WillReturnError(errors.New("fake error"))
dbMock.ExpectRollback()

err = testFunc(sqlDB)

Example test for the function above:

func Test_testFunc(t *testing.T) {
    convey.Convey("Test_testFunc exec failed", t, func() {
        sqlDB, dbMock, err := sqlmock.New()
        convey.So(err, convey.ShouldBeNil)
        dbMock.ExpectBegin()
        dbMock.ExpectQuery("select .* from test").WillReturnRows(sqlmock.NewRows([]string{"id", "num"}).AddRow(1, 10).AddRow(2, 20))
        dbMock.ExpectExec("update test").WillReturnError(errors.New("fake error"))
        dbMock.ExpectRollback()
        err = testFunc(sqlDB)
        convey.So(err, convey.ShouldNotBeNil)
        convey.So(err.Error(), convey.ShouldEqual, "fake error")
        convey.So(dbMock.ExpectationsWereMet(), convey.ShouldBeNil)
    })
}

When the project uses an ORM such as Gorm, a lightweight SQLite database can be created for tests:

import (
    "gorm.io/driver/sqlite"
    "gorm.io/gorm"
)

db, err := gorm.Open(sqlite.Open(""), &gorm.Config{})

Redis mocking with miniredis :

mr, err := miniredis.Run()
addr := mr.Addr()
opt, err := redis.ParseURL("redis://:@" + mr.Addr())
cli := redis.NewClient(opt)

Test setup and teardown using TestMain :

func TestMain(m *testing.M) {
    code := m.Run()
    if code == 0 {
        TearDone(true)
    } else {
        TearDone(false)
    }
    os.Exit(code)
}

func TearDone(isSuccess bool) {
    fmt.Println("Global test environment tear-down")
    if isSuccess {
        fmt.Println("[  PASSED  ]")
    } else {
        fmt.Println("[  FAILED  ]")
    }
}

Directories that contain generated mock code can be excluded from coverage reports, e.g.:

go test -v -covermode=count -coverprofile=coverage_unit.out '-gcflags=all=-N -l' `go list ./... | grep -v /mockdata`

Reflections on unit testing : Unit tests improve code quality and confidence during refactoring, but writing them can be painful when code is tightly coupled. Tests should focus on meaningful logic rather than achieving high coverage for its own sake. When dependencies are hard to mock, creating a lightweight local environment (SQLite, miniredis, or containerized services) can simplify testing.

RedisGounit testingMockDependency InjectionSQLitesqlmocktesting-frameworks
Tencent Cloud Developer
Written by

Tencent Cloud Developer

Official Tencent Cloud community account that brings together developers, shares practical tech insights, and fosters an influential tech exchange community.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.