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

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=")

Blogs

Book Recommendations