Enhetstestning i Go: Struktur & Bäst Praktik

Testa från grundläggande till avancerade mönster

Sidinnehåll

Gos inbyggda testpaket erbjuder ett kraftfullt, minimalistiskt ramverk för att skriva enhetstester utan externa beroenden. Här är grunderna för testning, projektstruktur och avancerade mönster för att bygga pålitliga Go-applikationer.

Go Unit Testing är fantastiskt

Varför testning är viktigt i Go

Gos filosofi betonar enkelhet och pålitlighet. Standardbiblioteket inkluderar testing-paketet, vilket gör enhetstestning till en förstaklassmedborgare i Go-ecosystemet. Vältestad Go-kod förbättrar underhållbarheten, upptäcker buggar tidigt och ger dokumentation genom exempel. Om du är ny på Go, kolla in vårt Go Cheat Sheet för en snabb referens av grundläggande språkfunktioner.

Nyckelfördelar med Go-testning:

  • Inbyggt stöd: Inga externa ramverk krävs
  • Snabb exekvering: Samtidig testkörning som standard
  • Enkel syntax: Minimal boilerplate-kod
  • Rikt verktygsutbud: Täckningsrapporter, prestandamätningar och profilering
  • CI/CD-vänlig: Lätt integration med automatiserade rör

Projektstruktur för Go-tester

Go-tester finns bredvid din produktionskod med tydliga namngivningskonventioner:

myproject/
├── go.mod
├── main.go
├── calculator.go
├── calculator_test.go
├── utils/
│   ├── helper.go
│   └── helper_test.go
└── models/
    ├── user.go
    └── user_test.go

Viktiga konventioner:

  • Testfiler slutar med _test.go
  • Tester är i samma paket som koden (eller använder _test-suffix för black-box-testning)
  • Varje källfil kan ha en motsvarande testfil

Pakettestningsmetoder

White-box-testning (samma paket):

package calculator

import "testing"
// Kan nå icke-exporterade funktioner och variabler

Black-box-testning (extern paket):

package calculator_test

import (
    "testing"
    "myproject/calculator"
)
// Kan bara nå exporterade funktioner (rekommenderas för offentliga API:er)

Grundläggande teststruktur

Varje testfunktion följer detta mönster:

package calculator

import "testing"

// Testfunktion måste börja med "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-metoder:

  • t.Error() / t.Errorf(): Märker test som misslyckat men fortsätter
  • t.Fatal() / t.Fatalf(): Märker test som misslyckat och stoppar omedelbart
  • t.Log() / t.Logf(): Loggar utdata (visas bara med -v-flaggan)
  • t.Skip() / t.Skipf(): Hoppar över testet
  • t.Parallel(): Kör test parallellt med andra parallella tester

Tabulärtestning: Det Go-iska sättet

Tabulärtestning är det idiomatiska Go-sättet för att testa flera scenarier. Med Go-generics kan du också skapa typ-säkra testhjälpmedel som fungerar över olika datatyper:

func TestCalculate(t *testing.T) {
    tests := []struct {
        name     string
        a, b     int
        op       string
        expected int
        wantErr  bool
    }{
        {"addition", 2, 3, "+", 5, false},
        {"subtraktion", 5, 3, "-", 2, false},
        {"multiplikation", 4, 3, "*", 12, false},
        {"division", 10, 2, "/", 5, false},
        {"division med noll", 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)
            }
        })
    }
}

Fördelar:

  • En testfunktion för flera scenarier
  • Lätt att lägga till nya testfall
  • Klar dokumentation av förväntat beteende
  • Bättre testorganisation och underhållbarhet

Körning av tester

Grundläggande kommandon

# Kör tester i aktuell katalog
go test

# Kör tester med detaljerad utdata
go test -v

# Kör tester i alla underkataloger
go test ./...

# Kör specifik test
go test -run TestAdd

# Kör tester som matchar mönster
go test -run TestCalculate/addition

# Kör tester parallellt (standard är GOMAXPROCS)
go test -parallel 4

# Kör tester med tidsgräns
go test -timeout 30s

Testtäckning

# Kör tester med täckningsmätning
go test -cover

# Generera täckningsprofil
go test -coverprofile=coverage.out

# Visa täckning i webbläsare
go tool cover -html=coverage.out

# Visa täckning per funktion
go tool cover -func=coverage.out

# Ställ in täckningsläge (set, count, atomic)
go test -covermode=count -coverprofile=coverage.out

Användbara flaggor

  • -short: Kör tester markerade med if testing.Short()-kontroller
  • -race: Aktivera race-detektor (hittar samtidiga åtkomstproblem)
  • -cpu: Ange GOMAXPROCS-värden
  • -count n: Kör varje test n gånger
  • -failfast: Stoppa vid första testmisslyckande

Testhjälpmedel och uppsättningsrensning

Hjälpfunktioner

Märk hjälpfunktioner med t.Helper() för att förbättra felrapportering:

func assertEqual(t *testing.T, got, want int) {
    t.Helper() // Den här raden rapporteras som anroparen
    if got != want {
        t.Errorf("got %d, want %d", got, want)
    }
}

func TestMath(t *testing.T) {
    result := Add(2, 3)
    assertEqual(t, result, 5) // Felraden pekar här
}

Uppsättning och rensning

func TestMain(m *testing.M) {
    // Uppsättningskod här
    setup()

    // Kör tester
    code := m.Run()

    // Rensningskod här
    teardown()

    os.Exit(code)
}

Testfixturer

func setupTestCase(t *testing.T) func(t *testing.T) {
    t.Log("uppsättning testfall")
    return func(t *testing.T) {
        t.Log("rensning testfall")
    }
}

func TestSomething(t *testing.T) {
    teardown := setupTestCase(t)
    defer teardown(t)

    // Testkod här
}

Mockning och beroendeinjektion

Gränssnittsbaserad mockning

När du testar kod som interagerar med databaser gör gränssnitt det enkelt att skapa mock-implementeringar. Om du arbetar med PostgreSQL i Go, se vårt jämförande av Go-ORM:er för att välja rätt databashanteringsbibliotek med bra testbarhet.

// Produktionskod
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
}

// Testkod
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("användare inte funnen")
}

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("oväntat fel: %v", err)
    }
    if name != "Alice" {
        t.Errorf("fick %s, förväntade Alice", name)
    }
}

Populära testbibliotek

Testify

Det mest populära Go-testbiblioteket för påståenden och mockar:

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, "de borde vara lika")
    assert.NotNil(t, result)
}

// Mock-exempel
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)
}

Andra verktyg

  • gomock: Googles mockningsramverk med kodgenerering
  • httptest: Standardbibliotek för testning av HTTP-hanterare
  • testcontainers-go: Integrationstestning med Docker-containrar
  • ginkgo/gomega: BDD-stilramverk för testning

När du testar integrationer med externa tjänster som AI-modeller, måste du mocka eller stubba dessa beroenden. Till exempel, om du använder Ollama i Go, överväg att skapa gränssnittshöljen för att göra din kod mer testbar.

Prestandatest

Go inkluderar inbyggt stöd för prestandatest:

func BenchmarkAdd(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Add(2, 3)
    }
}

// Kör prestandatest
// go test -bench=. -benchmem

Utdata visar iterationer per sekund och minnesallokeringar.

Bästa praxis

  1. Skriv tabulärtestning: Använd skivmönstret för flera testfall
  2. Använd t.Run för undertester: Bättre organisation och kan köra undertester selektivt
  3. Testa exporterade funktioner först: Fokusera på offentligt API-beteende
  4. Håll testerna enkla: Varje test bör verifiera en sak
  5. Använd meningsfulla testnamn: Beskriv vad som testas och förväntat resultat
  6. Testa inte implementeringsdetaljer: Testa beteende, inte internt
  7. Använd gränssnitt för beroenden: Gör mockning enklare
  8. Sträva efter hög täckning, men kvalitet framför kvantitet: 100% täckning betyder inte felfritt
  9. Kör tester med -race-flaggan: Upptäck samtidighetsproblem tidigt
  10. Använd TestMain för kostsam uppsättning: Undvik att upprepa uppsättning i varje test

Exempel: Komplett Testsvit

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("name cannot be empty")
    }
    if u.Email == "" {
        return errors.New("email cannot be empty")
    }
    return nil
}

// Testfil: user_test.go
func TestValidateUser(t *testing.T) {
    tests := []struct {
        name    string
        user    *User
        wantErr bool
        errMsg  string
    }{
        {
            name:    "valid user",
            user:    &User{ID: 1, Name: "Alice", Email: "alice@example.com"},
            wantErr: false,
        },
        {
            name:    "empty name",
            user:    &User{ID: 1, Name: "", Email: "alice@example.com"},
            wantErr: true,
            errMsg:  "name cannot be empty",
        },
        {
            name:    "empty email",
            user:    &User{ID: 1, Name: "Alice", Email: ""},
            wantErr: true,
            errMsg:  "email cannot be empty",
        },
    }

    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)
            }
        })
    }
}

Användbara länkar

Slutsats

Go’s testramverk ger allt som behövs för omfattande enhetstestning med minimal konfiguration. Genom att följa Go-idiomer som tabellstyrda tester, använda gränssnitt för mockning och utnyttja inbyggda verktyg kan du skapa underhållbara, pålitliga testsviter som växer med din kodbas.

Dessa testpraxis gäller för alla typer av Go-applikationer, från webbtjänster till CLI-applikationer byggda med Cobra & Viper. Att testa kommandoradsverktyg kräver liknande mönster med extra fokus på att testa inmatning/utmatning och flaggparsing.

Börja med enkla tester, lägg gradvis till täckning och kom ihåg att testning är en investering i kodkvalitet och utvecklarförtroende. Go-communityns fokus på testning gör det enklare att underhålla projekt långsiktigt och samarbeta effektivt med teammedlemmar.