Introduction: Why Testing in Go is Important
Go (Golang) is a language that from its very design has been focused on simplicity, performance, and reliability. The built-in testing package and powerful tools like go test make writing tests an integral part of development. Without quality testing, even the most elegant code eventually turns into a "fragile" system where one change breaks everything.
In this article, we will break down three key levels of testing: unit, integration, and e2e (end-to-end). You will learn how they differ, when to apply them, and how to write effective tests in Go. The material will be useful for both beginners and experienced developers looking to systematize their knowledge.
1. Unit Testing
Unit tests are the foundation of the testing pyramid. They check the operation of individual functions, methods, or modules in isolation from external dependencies (databases, network, file system).
Basic Principles of Unit Tests in Go
- Isolation: The code being tested should not depend on external services.
- Speed: Unit tests execute in milliseconds.
- Determinism: The test result should not change from one run to another.
Example of a Simple Unit Test
Suppose we have a function for adding two numbers:
// math.gopackage math
func Add(a, b int) int { return a + b}The test for this function would look like this:
// math_test.gopackage math
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) }}The test is run with the command: go test ./...
Table-driven Tests
The Go community prefers table-driven tests for checking multiple cases:
func TestAddTable(t *testing.T) { tests := []struct { a, b, expected int }{ {1, 1, 2}, {0, 0, 0}, {-1, 1, 0}, {100, 200, 300}, }
for _, tt := range tests { result := Add(tt.a, tt.b) if result != tt.expected { t.Errorf("Add(%d, %d) = %d; want %d", tt.a, tt.b, result, tt.expected) } }}Tip: Use t.Run() to create nested tests with clear names — this improves report readability.
2. Integration Testing
Integration tests check the interaction between system components: database, API, external services. Unlike unit tests, here we bring up real (or near-real) dependencies.
Features of Integration Tests in Go
- Often require environment setup (e.g., a test database).
- Can use build tags to separate unit and integration tests.
- Run slower, so they are executed separately.
Example: Testing Database Operations (PostgreSQL)
Let's create a file with the integration build tag:
// db_test.go//go:build integration
package db
import ( "database/sql" "testing")
func TestSaveUser(t *testing.T) { // Connect to the test database dsn := "host=localhost user=test password=test dbname=testdb sslmode=disable" db, err := sql.Open("postgres", dsn) if err != nil { t.Fatalf("failed to connect to db: %v", err) } defer db.Close()
// Clean data before the test _, _ = db.Exec("TRUNCATE TABLE users")
// Execute the operation being tested user := User{Name: "Alice", Email: "alice@example.com"} ```