Testing in Go: unit, integration, e2e from A to Z

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Introduction: Why testing in Go is a necessity?

\n

Go (Golang) is a modern programming language designed for building reliable and scalable systems. However, even the most elegant code is not immune to errors. That is why testing is an integral part of development in Go. The built-in testing package provides powerful tools for writing unit tests, integration tests, and even end-to-end (e2e) checks.

\n

In this article, we will cover all levels of testing in Go: from simple unit tests to complex e2e scenarios. You will learn how to properly structure tests, use mocks, write table-driven tests, and set up integration testing with real dependencies. The material will be useful for both beginners and experienced developers looking to systematize their knowledge.

\n\n

1. Unit testing in Go: the basics

\n

Unit tests check the operation of individual functions or methods in isolation from external dependencies. In Go, the standard testing package is used for this. The main rules: test files should be named *_test.go, and test functions should start with Test.

\n\n

1.1. Simple unit test

\n

Consider a function for adding two numbers:

\n
// math.go\npackage math\n\nfunc Add(a, b int) int {\n    return a + b\n}
\n

Test for it:

\n
// math_test.go\npackage math\n\nimport "testing"\n\nfunc TestAdd(t *testing.T) {\n    result := Add(2, 3)\n    expected := 5\n    if result != expected {\n        t.Errorf("Add(2, 3) = %d; want %d", result, expected)\n    }\n}
\n

Running tests: go test ./... or go test -v for verbose output.

\n\n

1.2. Table-driven tests

\n

The Go community actively uses the table-driven approach for testing multiple cases:

\n
func TestAddTableDriven(t *testing.T) {\n    tests := []struct {\n        name string\n        a, b int\n        want int\n    }{\n        {"positive", 2, 3, 5},\n        {"negative", -1, -2, -3},\n        {"zero", 0, 0, 0},\n    }\n    for _, tt := range tests {\n        t.Run(tt.name, func(t *testing.T) {\n            if got := Add(tt.a, tt.b); got != tt.want {\n                t.Errorf("Add() = %d, want %d", got, tt.want)\n            }\n        })\n    }\n}
\n

This approach makes tests easy to read and extend.

\n\n

1.3. Testing with errors

\n

For functions that return errors, use t.Fatal or t.Errorf:

\n
func TestDivide(t *testing.T) {\n    _, err := Divide(10, 0)\n    if err == nil {\n        t.Fatal("expected error for division by zero")\n    }\n}
\n\n

2. Integration testing: working with dependencies

\n

Integration tests check the interaction of components: databases, APIs, file systems. In Go, they are typically separated from unit tests using build tags or a separate package structure.

\n\n

2.1. Using build tags

\n

Create a file with the tag //go:build integration at the beginning:

\n
//go:build integration\n\npackage db\n\nimport (\n    "testing"\n    "database/sql"\n    _ "github.com/lib/pq"\n)\n\nfunc TestPostgresConnection(t *testing.T) {\n    dsn := "host=localhost port=5432 user=test password=test dbname=test sslmode=disable"\n    db, err := sql.Open("postgres", dsn)\n    if err != nil {\n        t.Fatalf("failed to open db: %v", err)\n    }\n    defer db.Close()\n    \n    if err := db.Ping(); err != nil {\n        t.Fatalf("failed to ping db: %v", err)\n    }\n}
\n

Running only integration tests: go test -tags=integration ./...

\n\n

2.2. Mocks and stubs

\n

To isolate tests, use interfaces and mocks. Example with the popular gomock library:

\n
// user.go\ntype UserRepository interface {\n    GetUser(id int) (*Us                

Blogs

Book Recommendations