Testing in Go: unit, integration, e2e — a complete guide

онлайн тренажер по питону
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 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"} ```

Blogs

Book Recommendations