Introduction: Why testing in Go is a necessity?
\nGo (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.
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\n1. Unit testing in Go: the basics
\nUnit 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.
1.1. Simple unit test
\nConsider a function for adding two numbers:
\n// math.go\npackage math\n\nfunc Add(a, b int) int {\n return a + b\n}\nTest 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}\nRunning tests: go test ./... or go test -v for verbose output.
1.2. Table-driven tests
\nThe Go community actively uses the table-driven approach for testing multiple cases:
\nfunc 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}\nThis approach makes tests easy to read and extend.
\n\n1.3. Testing with errors
\nFor functions that return errors, use t.Fatal or t.Errorf:
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\n2. Integration testing: working with dependencies
\nIntegration 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\n2.1. Using build tags
\nCreate a file with the tag //go:build integration at the beginning:
//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}\nRunning only integration tests: go test -tags=integration ./...
2.2. Mocks and stubs
\nTo isolate tests, use interfaces and mocks. Example with the popular gomock library:
// user.go\ntype UserRepository interface {\n GetUser(id int) (*Us