Introduction: Why testing in Go is fundamental
Go (Golang) was created with simplicity and reliability in mind. The built-in testing package is one of the best tools for writing tests among modern languages. It doesn't require third-party libraries for basic scenarios, yet it is easily extensible. In this article, we will cover three levels of testing: unit, integration, and e2e. You will learn how to write them, when to apply them, and which tools to use. The material will be useful for both beginners and experienced Go developers.
1. Unit tests in Go: the foundation
Unit tests check individual functions or methods in isolation. In Go, you just need to create a file with the _test.go suffix and write functions like TestXxx(t *testing.T).
Simple unit test example
Suppose we have an addition function:
// math.gopackage main
func Add(a, b int) int { return a + b}The test for it:
// math_test.gopackage main
import "testing"
func TestAdd(t *testing.T) { result := Add(2, 3) expected := 5 if result != expected { t.Errorf("Add(2, 3) = %d; want %d", result, expected) }}Table-Driven Tests
The Go community recommends using table-driven tests for checking multiple cases:
func TestAddTable(t *testing.T) { tests := []struct { name string a, b int want int }{ {"positive", 2, 3, 5}, {"negative", -1, -2, -3}, {"zero", 0, 0, 0}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := Add(tt.a, tt.b); got != tt.want { t.Errorf("Add() = %d, want %d", got, tt.want) } }) }}Unit tests are run with the command:
go test ./...2. Integration tests: checking interaction
Integration tests check how different parts of the system work together: database, API, file system. In Go, they are typically placed in separate files with a build tag or in an integration package.
Using build tags
Create a file integration_test.go with a tag:
//go:build integration
package integration
import ( "database/sql" "testing" _ "github.com/lib/pq")
func TestDatabaseConnection(t *testing.T) { dsn := "host=localhost port=5432 user=test dbname=test sslmode=disable" db, err := sql.Open("postgres", dsn) if err != nil { t.Fatalf("Failed to open DB: %v", err) } defer db.Close()
err = db.Ping() if err != nil { t.Fatalf("Failed to ping DB: %v", err) }}Running only integration tests:
go test -tags=integration ./...Working with a test database
For integration tests, testcontainers-go or in-memory databases (e.g., SQLite) are often used. Example with testcontainers:
func TestWithPostgresContainer(t *testing.T) { ctx := context.Background() pgContainer, err := postgres.RunContainer(ctx, testcontainers.WithImage("postgres:15"), postgres.WithDatabase("testdb"), ) require.NoError(t, err) defer pgContainer.Terminate(ctx)
dsn, _ := pgContainer.ConnectionString(ctx) // use dsn for connection}3. E2E tests: checking the entire system
End-to-End tests simulate user behavior and check the entire system from start to finish. In Go, they are often written for HTTP services using the net/http/httptest package.
E2E test example for a REST API
func TestCreateUserE2E(t *testing.T) { // S