Go-eenheidstesten: structuur en beste praktijken
Go-testen van basis tot geavanceerde patronen
Go’s built-in testing package biedt een krachtig, minimalistisch kader voor het schrijven van eenheidstests zonder externe afhankelijkheden. Hier zijn de basisprincipes van testen, projectstructuur en geavanceerde patronen om betrouwbare Go-toepassingen te bouwen.

Waarom testen belangrijk is in Go
Go’s filosofie benadrukt eenvoud en betrouwbaarheid. Het standaardbibliotheek bevat het testing-pakket, waardoor eenheidstesten een eerste klasse burger zijn in de Go-ecosysteem. Go-code die goed getest is verbetert onderhoudbaarheid, vangt fouten vroeg op en biedt documentatie via voorbeelden. Als je nieuw bent in Go, bekijk dan onze Go Cheat Sheet voor een snelle verwijzing naar de taalbasis.
Belangrijke voordelen van Go-testen:
- Ingebouwde ondersteuning: Geen externe frameworks vereist
- Snelle uitvoering: Concurrente testuitvoering als standaard
- Eenvoudige syntaxis: Minimale boilerplate-code
- Rijke tooling: Declaraties over dekking, benchmarks en profielering
- CI/CD-vriendelijk: Eenvoudige integratie met geautomatiseerde pijplijnen
Projectstructuur voor Go-tests
Go-tests bevinden zich naast je productiecode met een duidelijke naamgevingsconventie:
myproject/
├── go.mod
├── main.go
├── calculator.go
├── calculator_test.go
├── utils/
│ ├── helper.go
│ └── helper_test.go
└── models/
├── user.go
└── user_test.go
Belangrijke conventies:
- Testbestanden eindigen met
_test.go - Tests bevinden zich in hetzelfde pakket als de code (of gebruiken de
_test-suffix voor zwartkisttesten) - Elke bronbestand kan een overeenkomstig testbestand hebben
Pakkettestenbenaderingen
Witkisttesten (zelfde pakket):
package calculator
import "testing"
// Kan ongeëxporteerde functies en variabelen toegankelijk maken
Zwartkisttesten (extern pakket):
package calculator_test
import (
"testing"
"myproject/calculator"
)
// Kan alleen geëxporteerde functies toegankelijk maken (aangeraden voor openbare API's)
Basisstructuur van een test
Elke testfunctie volgt dit patroon:
package calculator
import "testing"
// Testfunctie moet beginnen met "Test"
func TestAdd(t *testing.T) {
result := Add(2, 3)
expected := 5
if result != expected {
t.Errorf("Add(2, 3) = %d; want %d", result, expected)
}
}
Testing.T methoden:
t.Error()/t.Errorf(): Markeer test als mislukt maar voortzettent.Fatal()/t.Fatalf(): Markeer test als mislukt en stop directt.Log()/t.Logf(): Loguitvoer (alleen zichtbaar met-v-vlag)t.Skip()/t.Skipf(): Overslaan van de testt.Parallel(): Voer test parallel uit met andere parallelle tests
Tabelgeleide tests: De Go-achtige manier
Tabelgeleide tests zijn de idiomatische Go-achtige aanpak voor het testen van meerdere scenario’s. Met Go generics, kun je ook typesafe testhelpers maken die werken over verschillende datatypes:
func TestCalculate(t *testing.T) {
tests := []struct {
name string
a, b int
op string
expected int
wantErr bool
}{
{"additie", 2, 3, "+", 5, false},
{"subtraheren", 5, 3, "-", 2, false},
{"vermenigvuldigen", 4, 3, "*", 12, false},
{"delen", 10, 2, "/", 5, false},
{"delen door nul", 10, 0, "/", 0, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := Calculate(tt.a, tt.b, tt.op)
if (err != nil) != tt.wantErr {
t.Errorf("Calculate() error = %v, wantErr %v", err, tt.wantErr)
return
}
if result != tt.expected {
t.Errorf("Calculate(%d, %d, %q) = %d; want %d",
tt.a, tt.b, tt.op, result, tt.expected)
}
})
}
}
Voordelen:
- Een enkele testfunctie voor meerdere scenario’s
- Eenvoudig toevoegen van nieuwe testgevallen
- Duidelijke documentatie van verwachte gedrag
- Betere testorganisatie en onderhoudbaarheid
Tests uitvoeren
Basiscommando’s
# Tests uitvoeren in huidige map
go test
# Tests uitvoeren met uitgebreide uitvoer
go test -v
# Tests uitvoeren in alle submappen
go test ./...
# Specifieke test uitvoeren
go test -run TestAdd
# Tests uitvoeren met patroon
go test -run TestCalculate/additie
# Tests uitvoeren parallel (standaard is GOMAXPROCS)
go test -parallel 4
# Tests uitvoeren met timeout
go test -timeout 30s
Testdekking
# Tests uitvoeren met dekking
go test -cover
# Dekking profiel genereren
go test -coverprofile=coverage.out
# Dekking in browser bekijken
go tool cover -html=coverage.out
# Dekking per functie tonen
go tool cover -func=coverage.out
# Dekkingmodus instellen (set, count, atomic)
go test -covermode=count -coverprofile=coverage.out
Nuttige vlaggen
-short: Voer tests uit gemarkeerd metif testing.Short()checks-race: Race detector inschakelen (vindt concurrentieproblemen)-cpu: GOMAXPROCS-waarden opgeven-count n: Elke test n keer uitvoeren-failfast: Stop bij eerste testmislukking
Testhelpers en opzet/afsluiten
Helperfuncties
Markeer helperfuncties met t.Helper() om foutmeldingen te verbeteren:
func assertEqual(t *testing.T, got, want int) {
t.Helper() // Deze regel wordt als de aanroeper gemeld
if got != want {
t.Errorf("got %d, want %d", got, want)
}
}
func TestMath(t *testing.T) {
result := Add(2, 3)
assertEqual(t, result, 5) // Foutregel wijst hierheen
}
Opzet en afsluiten
func TestMain(m *testing.M) {
// Opzetcode hier
setup()
// Tests uitvoeren
code := m.Run()
// Afsluitcode hier
teardown()
os.Exit(code)
}
Testfixture’s
func setupTestCase(t *testing.T) func(t *testing.T) {
t.Log("testgeval opzetten")
return func(t *testing.T) {
t.Log("testgeval afsluiten")
}
}
func TestSomething(t *testing.T) {
teardown := setupTestCase(t)
defer teardown(t)
// Testcode hier
}
Mocken en afhankelijkheidinjectie
Interfacegebaseerd mocken
Wanneer je code test die interactie heeft met databases, maakt het gebruik van interfaces het gemakkelijk om mockimplementaties te maken. Als je werkt met PostgreSQL in Go, zie dan onze vergelijking van Go ORMs voor het kiezen van het juiste databaselib met goede testbaarheid.
// Productiecode
type Database interface {
GetUser(id int) (*User, error)
}
type UserService struct {
db Database
}
func (s *UserService) GetUserName(id int) (string, error) {
user, err := s.db.GetUser(id)
if err != nil {
return "", err
}
return user.Name, nil
}
// Testcode
type MockDatabase struct {
users map[int]*User
}
func (m *MockDatabase) GetUser(id int) (*User, error) {
if user, ok := m.users[id]; ok {
return user, nil
}
return nil, errors.New("gebruiker niet gevonden")
}
func TestGetUserName(t *testing.T) {
mockDB := &MockDatabase{
users: map[int]*User{
1: {ID: 1, Name: "Alice"},
},
}
service := &UserService{db: mockDB}
name, err := service.GetUserName(1)
if err != nil {
t.Fatalf("onverwachte fout: %v", err)
}
if name != "Alice" {
t.Errorf("kreeg %s, wilde Alice", name)
}
}
Populaire testbibliotheken
Testify
De meest populaire Go-testbibliotheek voor beweringen en mocks:
import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
func TestWithTestify(t *testing.T) {
result := Add(2, 3)
assert.Equal(t, 5, result, "ze moeten gelijk zijn")
assert.NotNil(t, result)
}
// Mockvoorbeeld
type MockDB struct {
mock.Mock
}
func (m *MockDB) GetUser(id int) (*User, error) {
args := m.Called(id)
return args.Get(0).(*User), args.Error(1)
}
Andere tools
- gomock: Google’s mockframework met codegeneratie
- httptest: Standaardbibliotheek voor het testen van HTTP-handlers
- testcontainers-go: Integratietesten met Docker-containers
- ginkgo/gomega: BDD-stijl testframework
Wanneer je integraties test met externe diensten zoals AI-modellen, zul je die afhankelijkheden moeten mocken of stubben. Bijvoorbeeld, als je Ollama in Go gebruikt, overweeg dan het maken van interface wrappers om je code beter te testen.
Benchmarktests
Go biedt ingebouwde ondersteuning voor benchmarks:
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(2, 3)
}
}
// Benchmarks uitvoeren
// go test -bench=. -benchmem
De uitvoer toont iteraties per seconde en geheugenallocaties.
Beste praktijken
- Schrijf tabelgeleide tests: Gebruik het slice van structs patroon voor meerdere testgevallen
- Gebruik t.Run voor subtests: Betere organisatie en kan subtests selectief uitvoeren
- Test eerst geëxporteerde functies: Focus op het gedrag van de openbare API
- Houd tests eenvoudig: Elke test moet één ding controleren
- Gebruik betekenisvolle testnamen: Beschrijf wat getest wordt en verwachte uitkomst
- Test geen implementatiedetails: Test gedrag, niet interne details
- Gebruik interfaces voor afhankelijkheden: Maakt mocken gemakkelijker
- Streef naar hoge dekking, maar kwaliteit boven kwantiteit: 100% dekking betekent niet foutvrij
- Voer tests uit met -race vlag: Vang concurrentieproblemen vroeg op
- Gebruik TestMain voor duurzame opzet: Vermijd herhalen van opzet in elke test
Voorbeeld: Volledige testsuite
package user
import (
"errors"
"testing"
)
type User struct {
ID int
Name string
Email string
}
func ValidateUser(u *User) error {
if u.Name == "" {
return errors.New("naam kan niet leeg zijn")
}
if u.Email == "" {
return errors.New("email kan niet leeg zijn")
}
return nil
}
// Testbestand: user_test.go
func TestValidateUser(t *testing.T) {
tests := []struct {
name string
user *User
wantErr bool
errMsg string
}{
{
name: "geldige gebruiker",
user: &User{ID: 1, Name: "Alice", Email: "alice@example.com"},
wantErr: false,
},
{
name: "lege naam",
user: &User{ID: 1, Name: "", Email: "alice@example.com"},
wantErr: true,
errMsg: "naam kan niet leeg zijn",
},
{
name: "lege email",
user: &User{ID: 1, Name: "Alice", Email: ""},
wantErr: true,
errMsg: "email kan niet leeg zijn",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateUser(tt.user)
if (err != nil) != tt.wantErr {
t.Errorf("ValidateUser() error = %v, wantErr %v", err, tt.wantErr)
return
}
if err != nil && err.Error() != tt.errMsg {
t.Errorf("ValidateUser() error message = %v, want %v", err.Error(), tt.errMsg)
}
})
}
}
Nuttige links
- Officiële Go Test Package Documentatie
- Go Blog: Tabelgeleide tests
- Testify GitHub Repository
- GoMock Documentatie
- Leer Go met tests
- Go Code Coverage Tool
- Go Cheat Sheet
- Vergelijking van Go ORMs voor PostgreSQL: GORM vs Ent vs Bun vs sqlc
- Go SDKs voor Ollama - vergelijking met voorbeelden
- CLI-applicaties bouwen in Go met Cobra & Viper
- Go Generics: Gebruikscases en patronen
Conclusie
Go’s testframework biedt alles wat nodig is voor uitgebreide eenheidstesten met minimale opzet. Door Go-idiomen zoals tabelgeleide tests te volgen, interfaces te gebruiken voor mocken en ingebouwde tools te benutten, kun je onderhoudbare, betrouwbare testsuites maken die groeien met je codebasis.
Deze testpraktijken zijn van toepassing op alle soorten Go-toepassingen, van webdiensten tot CLI-applicaties gebouwd met Cobra & Viper. Het testen van command-line tools vereist vergelijkbare patronen met extra aandacht voor het testen van invoer/uitvoer en vlaggenverwerking.
Begin met eenvoudige tests, voeg geleidelijk dekking toe en onthoud dat testen een investering is in codekwaliteit en ontwikkelaarsvertrouwen. De nadruk van de Go-community op testen maakt het makkelijker om projecten op lange termijn te onderhouden en effectief samen te werken met teamleden.