Testing in Go: Unit, Integration, E2E — Complete Guide
Testing is an integral part of developing quality software. In the Go language (Golang), testing is built into the standard library, making it simple and effective. In this article, we will break down three main levels of testing: unit, integration, and e2e tests. You will learn how to write them, when to apply them, and which tools to use.
Why is testing in Go so important?
Go was created with a focus on simplicity and reliability. The built-in testing package and the go test tool make it easy to add tests to any project. The Go team adheres to the philosophy: "If the code isn't tested, it doesn't work." Testing helps:
- Detect errors early in the development process
- Simplify code refactoring and maintenance
- Document the behavior of functions and modules
- Increase confidence in application stability
Unit testing in Go
Unit tests check the operation of individual functions, methods, or structures in isolation from external dependencies. In Go, they are written in files with the _test.go suffix.
Basics of unit tests
Let's look at a simple example. Suppose we have an addition function:
// math.gopackage main
func Add(a, b int) int { return a + b}The test for this function would look like this:
// 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) }}Running the test: go test -v
Table-driven tests
Go encourages the use of table-driven tests to check multiple scenarios:
func TestAddTableDriven(t *testing.T) { tests := []struct { name string a, b int want int }{ {"positive numbers", 2, 3, 5}, {"negative numbers", -1, -2, -3}, {"zero", 0, 0, 0}, {"mixed", -5, 10, 5}, }
for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := Add(tt.a, tt.b) if got != tt.want { t.Errorf("Add(%d, %d) = %d; want %d", tt.a, tt.b, got, tt.want) } }) }}Testing with Mocks
To isolate unit tests, mocks are often required. In Go, you can use interfaces and libraries such as gomock or testify/mock.
// Define the interfacetype UserRepository interface { GetUser(id int) (User, error)}
// Test using testifyfunc TestGetUserName(t *testing.T) { mockRepo := new(MockUserRepository) mockRepo.On("GetUser", 1).Return(User{Name: "Alice"}, nil)
service := NewUserService(mockRepo) name, err := service.GetUserName(1)
assert.NoError(t, err) assert.Equal(t, "Alice", name) mockRepo.AssertExpectations(t)}Integration testing in Go
Integration tests check the interaction between system components: databases, APIs, file systems, and other external services. In Go, it is common to place them in separate packages or use build tags.
Testing with a database
For testing database interactions, testcontainers or temporary databases are often used:
// integration_test.go (with build tag)//go:build integration
package integration
import ( "database/sql" "testing" _ "github.com/lib/pq")
func TestUserRepository(t *testing.T) { // Connection to test DB db, err := sql.Open("postgres", "host=localhost port=")