Go-Testing mit Testify: Produktionsmuster für Unit-Tests und Best Practices 2026

编程语言

Wenn if err != nil auf reflect.DeepEqual trifft: Der Go-Testing-Schmerzkreislauf

Haben Sie schon Tests wie diesen geschrieben?

func TestAdd(t *testing.T) {
    result := Add(1, 2)
    if result != 3 {
        t.Errorf("Add(1, 2) = %d; want 3", result)
    }
}

Ein einfacher Additionstest sieht in Ordnung aus. Aber wenn Sie Slice-Gleichheit, Map-Vergleiche, tiefe Strukturvergleiche oder Fehlertyp-Zuordnung testen müssen, wird der Code zum Albtraum — reflect.DeepEqual überall, Fehlermeldungen ohne jede Lesbarkeit, und wenn Tests fehlschlagen, wissen Sie nur „nicht gleich", ohne zu wissen, was unterschiedlich ist. Ganz zu schweigen davon, dass Go-Standardbibliothek kein eingebautes Mock-Framework hat, Interface-Simulation erfolgt komplett manuell und die Testorganisation besteht aus aufgetürmten if-Anweisungen.

Das ist kein Einzelfall. Bei Google, Uber und anderen großen Unternehmen mit Go-Projekten ist Testcode ohne Testify oft länger als der Geschäftscode selbst. Dieser Artikel behandelt 5 produktionsreife Testmuster, die Ihnen helfen, das Testify-Framework zu meistern und Ihre Go-Tests von „läuft irgendwie" zu „professionell und zuverlässig" zu entwickeln.


Kernkonzepte-Referenz

Komponente Zweck Hauptmerkmale Typischer Anwendungsfall
assert Assertionen durchführen und Fehler aufzeichnen Setzt nach Fehler fort, sammelt alle Fehler Unit-Tests mit mehreren Assertionen
require Assertionen durchführen und sofort stoppen Stoppt aktuellen Test bei Fehler, vermeidet Nil-Pointer-Kaskaden Vorbedingungsprüfungen
suite Test-Suite-Organisation Setup/TearDown-Lebenszyklus, geteilter Zustand Integrationstests, Datenbanktests
mock Interface-Simulation Automatisch generierte Methodenaufruf-Assertionen Abhängigkeitsentkopplung, externe Dienst-Simulation
httptest HTTP-Testserver Echte HTTP-Anfragesimulation ohne Serverstart API-Handler-Tests

5 Herausforderungen beim Go-Testing

Herausforderung 1: Verbose Assertionen mit wenig aussagekräftigen Meldungen

func TestUserEqual(t *testing.T) {
    got := GetUser(1)
    want := User{ID: 1, Name: "Alice", Age: 30}
    if got.ID != want.ID {
        t.Errorf("ID: got %d, want %d", got.ID, want.ID)
    }
    if got.Name != want.Name {
        t.Errorf("Name: got %s, want %s", got.Name, want.Name)
    }
    if got.Age != want.Age {
        t.Errorf("Age: got %d, want %d", got.Age, want.Age)
    }
}

Drei Felder erfordern drei if-Blöcke. Was ist mit zehn Feldern? Verschachtelten Strukturen? reflect.DeepEqual kann vergleichen, aber die Fehlermeldung sagt nur not equal — Sie haben keine Ahnung, welches Feld abweicht.

Herausforderung 2: Kein eingebautes Mock, handgeschriebener Simulationscode explodiert

type UserRepository interface {
    GetByID(ctx context.Context, id int64) (*User, error)
    Create(ctx context.Context, user *User) error
    Update(ctx context.Context, user *User) error
    Delete(ctx context.Context, id int64) error
    List(ctx context.Context, filter Filter) ([]*User, error)
}

Ein Interface mit 5 Methoden erfordert die Implementierung von 5 Methoden für einen handgeschriebenen Mock, selbst wenn ein Test nur 1 verwendet. Zehn Interfaces bedeuten 50 Methodenimplementierungen — Mock-Code übersteigt den Geschäftscode.

Herausforderung 3: Unorganisierte Tests, wiederholtes Setup/TearDown

Jeder Test benötigt db.Connect(), jedes Testende benötigt db.Close(), plus Testdatenbereinigung. Überall Copy-Paste, und fehlendes TearDown verunreinigt den nächsten Test.

Herausforderung 4: Dilemma der tabellengesteuerten Test-Assertionen

Tabellengesteuerte Tests sind Go's idiomatisches Muster, aber Standardbibliotheks-Assertionen schneiden schlecht ab — t.Errorf sagt Ihnen nicht, welcher spezifische Testfall fehlgeschlagen ist; Sie können nur durch manuelles Hinzufügen von Fallnamen unterscheiden.

Herausforderung 5: HTTP-Tests hängen von echten Diensten ab

API-Handler testen — starten Sie einen HTTP-Server? Verwenden Sie httptest.NewRecorder? Wie konstruiert man Request-Bodies? Wie assertiert man Responses? Kein einheitliches Muster bedeutet, dass zehn Teammitglieder zehn verschiedene Stile schreiben.


Muster 1: assert vs require — Zwei Philosophien der Assertion

assert vs require — Kernunterschiede

package user_test

import (
    "testing"
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"
)

func TestAssertVsRequire(t *testing.T) {
    // assert: continues after failure, collects all errors
    assert.Equal(t, 1, 2, "first assertion")
    assert.Equal(t, 3, 3, "second assertion") // still executes
    assert.NotNil(t, nil, "third assertion")    // still executes
    // Test result: 3 failures, you see all problems at once
}

func TestRequireBehavior(t *testing.T) {
    // require: stops current test immediately on failure
    require.Equal(t, 1, 2, "precondition not met")
    // This line never executes
    require.Equal(t, 3, 3, "won't reach here")
}

Produktionsreife Assertionspraxis

package service_test

import (
    "testing"
    "time"

    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"
    "myapp/model"
    "myapp/service"
)

func TestCreateUser(t *testing.T) {
    // require for preconditions: creation must succeed, otherwise subsequent assertions are meaningless
    user, err := service.CreateUser("alice", "alice@example.com")
    require.NoError(t, err, "creating user should not return an error")
    require.NotNil(t, user, "creating user should not return nil")

    // assert for business assertions: even if one field is wrong, you want to see other field results
    assert.Equal(t, "alice", user.Name, "username should match")
    assert.Equal(t, "alice@example.com", user.Email, "email should match")
    assert.NotZero(t, user.ID, "user ID should not be zero")
    assert.WithinDuration(t, time.Now(), user.CreatedAt, time.Second, "creation time should be close to now")
}

Referenz gängiger Assertionsmethoden

// Equality
assert.Equal(t, expected, actual)
assert.NotEqual(t, expected, actual)
assert.Exactly(t, expected, actual) // type + value exactly the same

// Boolean
assert.True(t, condition)
assert.False(t, condition)

// Nil checks
assert.Nil(t, obj)
assert.NotNil(t, obj)

// Error checks
assert.NoError(t, err)
assert.Error(t, err)
assert.ErrorIs(t, err, os.ErrNotExist)
assert.ErrorContains(t, err, "not found")

// Collections
assert.Contains(t, slice, element)
assert.NotContains(t, slice, element)
assert.ElementsMatch(t, []int{1,2,3}, []int{3,1,2}) // unordered match
assert.Subset(t, []int{1,2,3,4}, []int{2,3})

// Type checks
assert.IsType(t, &User{}, actual)
assert.Implements(t, (*Repository)(nil), actual)

// Numeric comparisons
assert.Greater(t, 10, 5)
assert.LessOrEqual(t, 5, 5)
assert.InDelta(t, 3.14, 3.14159, 0.01)

// Strings
assert.Empty(t, "")
assert.Regexp(t, `^\d+$`, "12345")

Muster 2: Tabellengesteuerte Tests + testify suite

Basistabellengesteuerter Test

package calculator_test

import (
    "testing"
    "github.com/stretchr/testify/assert"
    "myapp/calculator"
)

func TestDivide(t *testing.T) {
    tests := []struct {
        name        string
        a           float64
        b           float64
        want        float64
        wantErr     bool
        errContains string
    }{
        {
            name:    "normal division",
            a:       10.0,
            b:       2.0,
            want:    5.0,
            wantErr: false,
        },
        {
            name:        "divide by zero",
            a:           10.0,
            b:           0.0,
            wantErr:     true,
            errContains: "division by zero",
        },
        {
            name:    "negative division",
            a:       -10.0,
            b:       2.0,
            want:    -5.0,
            wantErr: false,
        },
        {
            name:    "float precision",
            a:       1.0,
            b:       3.0,
            want:    0.333333,
            wantErr: false,
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got, err := calculator.Divide(tt.a, tt.b)
            if tt.wantErr {
                assert.Error(t, err)
                assert.Contains(t, err.Error(), tt.errContains)
                return
            }
            assert.NoError(t, err)
            assert.InDelta(t, tt.want, got, 0.001)
        })
    }
}

testify suite: Test-Suite mit Lebenszyklus

package repository_test

import (
    "context"
    "testing"

    "github.com/stretchr/testify/suite"
    "myapp/model"
    "myapp/repository"
    "myapp/testutil"
)

type UserRepositorySuite struct {
    suite.Suite // Embed suite.Suite to get assert/require methods

    db   *testutil.TestDB
    repo repository.UserRepository
    ctx  context.Context
}

// SetupSuite runs once before all tests
func (s *UserRepositorySuite) SetupSuite() {
    s.db = testutil.NewTestDB()
    s.db.Migrate()
    s.ctx = context.Background()
}

// TearDownSuite runs once after all tests
func (s *UserRepositorySuite) TearDownSuite() {
    s.db.Close()
}

// SetupTest runs before each test
func (s *UserRepositorySuite) SetupTest() {
    s.db.TruncateTables()
    s.repo = repository.NewUserRepository(s.db.Conn())
}

// TearDownTest runs after each test
func (s *UserRepositorySuite) TearDownTest() {
    // Clean up caches etc.
}

func (s *UserRepositorySuite) TestCreate() {
    user := &model.User{
        Name:  "Alice",
        Email: "alice@example.com",
    }

    err := s.repo.Create(s.ctx, user)
    s.NoError(err)
    s.NotZero(user.ID)

    found, err := s.repo.GetByID(s.ctx, user.ID)
    s.NoError(err)
    s.Equal("Alice", found.Name)
    s.Equal("alice@example.com", found.Email)
}

func (s *UserRepositorySuite) TestGetByIDNotFound() {
    _, err := s.repo.GetByID(s.ctx, 99999)
    s.Error(err)
    s.ErrorIs(err, repository.ErrNotFound)
}

func (s *UserRepositorySuite) TestList() {
    users := []*model.User{
        {Name: "Alice", Email: "alice@example.com"},
        {Name: "Bob", Email: "bob@example.com"},
        {Name: "Charlie", Email: "charlie@example.com"},
    }
    for _, u := range users {
        s.NoError(s.repo.Create(s.ctx, u))
    }

    result, err := s.repo.List(s.ctx, repository.Filter{Limit: 10})
    s.NoError(err)
    s.Len(result, 3)
}

// Entry function: must call suite.Run
func TestUserRepositorySuite(t *testing.T) {
    suite.Run(t, new(UserRepositorySuite))
}

Muster 3: Mock und Interface-Simulation

testify/mock — Grundlegende Verwendung

package service_test

import (
    "context"
    "testing"

    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/mock"
    "github.com/stretchr/testify/require"
    "myapp/model"
    "myapp/service"
)

// MockUserRepository simulates the UserRepository interface
type MockUserRepository struct {
    mock.Mock
}

func (m *MockUserRepository) GetByID(ctx context.Context, id int64) (*model.User, error) {
    args := m.Called(ctx, id)
    if args.Get(0) == nil {
        return nil, args.Error(1)
    }
    return args.Get(0).(*model.User), args.Error(1)
}

func (m *MockUserRepository) Create(ctx context.Context, user *model.User) error {
    args := m.Called(ctx, user)
    return args.Error(0)
}

func (m *MockUserRepository) Update(ctx context.Context, user *model.User) error {
    args := m.Called(ctx, user)
    return args.Error(0)
}

func (m *MockUserRepository) Delete(ctx context.Context, id int64) error {
    args := m.Called(ctx, id)
    return args.Error(0)
}

func (m *MockUserRepository) List(ctx context.Context, filter repository.Filter) ([]*model.User, error) {
    args := m.Called(ctx, filter)
    if args.Get(0) == nil {
        return nil, args.Error(1)
    }
    return args.Get(0).([]*model.User), args.Error(1)
}

Service-Layer-Tests: Aufrufverhalten verifizieren

func TestUserService_GetUser(t *testing.T) {
    mockRepo := new(MockUserRepository)
    svc := service.NewUserService(mockRepo)

    t.Run("user exists", func(t *testing.T) {
        expectedUser := &model.User{ID: 1, Name: "Alice", Email: "alice@example.com"}

        mockRepo.On("GetByID", mock.Anything, int64(1)).
            Return(expectedUser, nil).Once()

        user, err := svc.GetUser(context.Background(), 1)

        require.NoError(t, err)
        assert.Equal(t, "Alice", user.Name)

        mockRepo.AssertExpectations(t)
    })

    t.Run("user not found", func(t *testing.T) {
        mockRepo.On("GetByID", mock.Anything, int64(999)).
            Return(nil, repository.ErrNotFound).Once()

        _, err := svc.GetUser(context.Background(), 999)

        assert.ErrorIs(t, err, repository.ErrNotFound)
        mockRepo.AssertExpectations(t)
    })
}

func TestUserService_CreateUser_DuplicateEmail(t *testing.T) {
    mockRepo := new(MockUserRepository)
    svc := service.NewUserService(mockRepo)

    mockRepo.On("Create", mock.Anything, mock.MatchedBy(func(u *model.User) bool {
        return u.Email == "duplicate@example.com"
    })).Return(repository.ErrDuplicateEmail).Once()

    err := svc.CreateUser(context.Background(), "Alice", "duplicate@example.com")

    assert.ErrorIs(t, err, repository.ErrDuplicateEmail)
    mockRepo.AssertExpectations(t)
}

Erweiterte Mocks: Matcher und Aufrufanzahlen

func TestUserService_ComplexMock(t *testing.T) {
    mockRepo := new(MockUserRepository)
    svc := service.NewUserService(mockRepo)

    mockRepo.On("Create",
        mock.Anything,
        mock.MatchedBy(func(u *model.User) bool {
            return u.Name != "" && u.Email != ""
        }),
    ).Return(nil).Once()

    mockRepo.On("Delete", mock.Anything, mock.Anything).Return(nil).Times(0)

    err := svc.CreateUser(context.Background(), "Alice", "alice@example.com")
    require.NoError(t, err)

    mockRepo.AssertExpectations(t)

    mockRepo.AssertCalled(t, "Create", mock.Anything, mock.Anything)
    mockRepo.AssertNumberOfCalls(t, "Create", 1)

    mockRepo.AssertNotCalled(t, "Delete", mock.Anything, mock.Anything)
}

Muster 4: HTTP-Handler-Tests

Gin-Handler mit httptest testen

package handler_test

import (
    "encoding/json"
    "net/http"
    "net/http/httptest"
    "strings"
    "testing"

    "github.com/gin-gonic/gin"
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"
    "myapp/handler"
    "myapp/model"
)

func init() {
    gin.SetMode(gin.TestMode)
}

func TestGetUserHandler(t *testing.T) {
    mockSvc := new(MockUserService)
    h := handler.NewUserHandler(mockSvc)

    router := gin.New()
    router.GET("/api/v1/users/:id", h.GetUser)

    t.Run("successfully get user", func(t *testing.T) {
        expectedUser := &model.User{ID: 1, Name: "Alice", Email: "alice@example.com"}
        mockSvc.On("GetUser", mock.Anything, int64(1)).Return(expectedUser, nil).Once()

        w := httptest.NewRecorder()
        req, _ := http.NewRequest("GET", "/api/v1/users/1", nil)
        router.ServeHTTP(w, req)

        assert.Equal(t, http.StatusOK, w.Code)

        var resp struct {
            Code int         `json:"code"`
            Data *model.User `json:"data"`
        }
        err := json.Unmarshal(w.Body.Bytes(), &resp)
        require.NoError(t, err)
        assert.Equal(t, 0, resp.Code)
        assert.Equal(t, "Alice", resp.Data.Name)
        mockSvc.AssertExpectations(t)
    })

    t.Run("user not found returns 404", func(t *testing.T) {
        mockSvc.On("GetUser", mock.Anything, int64(999)).
            Return(nil, service.ErrUserNotFound).Once()

        w := httptest.NewRecorder()
        req, _ := http.NewRequest("GET", "/api/v1/users/999", nil)
        router.ServeHTTP(w, req)

        assert.Equal(t, http.StatusNotFound, w.Code)
        mockSvc.AssertExpectations(t)
    })
}

func TestCreateUserHandler(t *testing.T) {
    mockSvc := new(MockUserService)
    h := handler.NewUserHandler(mockSvc)

    router := gin.New()
    router.POST("/api/v1/users", h.CreateUser)

    t.Run("create successfully", func(t *testing.T) {
        mockSvc.On("CreateUser", mock.Anything, "Alice", "alice@example.com").
            Return(&model.User{ID: 1, Name: "Alice", Email: "alice@example.com"}, nil).Once()

        body := `{"name":"Alice","email":"alice@example.com"}`
        w := httptest.NewRecorder()
        req, _ := http.NewRequest("POST", "/api/v1/users", strings.NewReader(body))
        req.Header.Set("Content-Type", "application/json")
        router.ServeHTTP(w, req)

        assert.Equal(t, http.StatusCreated, w.Code)
        mockSvc.AssertExpectations(t)
    })

    t.Run("validation failure", func(t *testing.T) {
        body := `{"name":"","email":"invalid"}`
        w := httptest.NewRecorder()
        req, _ := http.NewRequest("POST", "/api/v1/users", strings.NewReader(body))
        req.Header.Set("Content-Type", "application/json")
        router.ServeHTTP(w, req)

        assert.Equal(t, http.StatusBadRequest, w.Code)
    })
}

httptest.NewServer: Externe HTTP-Dienste simulieren

package client_test

import (
    "context"
    "net/http"
    "net/http/httptest"
    "testing"

    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"
    "myapp/client"
)

func TestWeatherClient_GetWeather(t *testing.T) {
    server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        assert.Equal(t, "/api/weather", r.URL.Path)
        assert.Equal(t, "Bearer test-token", r.Header.Get("Authorization"))
        assert.Equal(t, "Beijing", r.URL.Query().Get("city"))

        w.Header().Set("Content-Type", "application/json")
        w.WriteHeader(http.StatusOK)
        _, _ = w.Write([]byte(`{"city":"Beijing","temp":25,"humidity":60}`))
    }))
    defer server.Close()

    c := client.NewWeatherClient(server.URL, "test-token")

    weather, err := c.GetWeather(context.Background(), "Beijing")

    require.NoError(t, err)
    assert.Equal(t, "Beijing", weather.City)
    assert.Equal(t, 25, weather.Temp)
    assert.Equal(t, 60, weather.Humidity)
}

Muster 5: Test-Suite-Organisation und CI-Integration

Projekt-Test-Verzeichnisstruktur

myapp/
├── internal/
│   ├── user/
│   │   ├── service.go
│   │   ├── service_test.go
│   │   ├── handler.go
│   │   ├── handler_test.go
│   │   ├── mock_repository.go
│   │   └── testdata/
│   │       ├── create_user_req.json
│   │       └── get_user_resp.json
│   └── order/
│       ├── service.go
│       ├── service_test.go
│       └── suite_test.go
├── integration/
│   └── user_repository_test.go
├── testutil/
│   ├── db.go
│   └── fixture.go
├── Makefile
└── .github/
    └── workflows/
        └── test.yml

Makefile-Testbefehle

.PHONY: test test-unit test-integration test-coverage test-race

test-unit:
	go test -v -count=1 -short ./internal/...

test-integration:
	go test -v -count=1 -tags=integration ./integration/...

test-race:
	go test -v -race -count=1 ./...

test-coverage:
	go test -coverprofile=coverage.out -covermode=atomic ./...
	go tool cover -html=coverage.out -o coverage.html
	@echo "Coverage: $$(go tool cover -func=coverage.out | grep total | awk '{print $$3}')"

test: test-unit test-coverage

GitHub Actions CI-Konfiguration

name: Test

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        go-version: ['1.24', '1.25']
    steps:
      - uses: actions/checkout@v4

      - name: Setup Go
        uses: actions/setup-go@v5
        with:
          go-version: ${{ matrix.go-version }}

      - name: Cache Go modules
        uses: actions/cache@v4
        with:
          path: |
            ~/go/pkg/mod
            ~/.cache/go-build
          key: ${{ runner.os }}-go-${{ matrix.go-version }}-${{ hashFiles('**/go.sum') }}

      - name: Download dependencies
        run: go mod download

      - name: Vet
        run: go vet ./...

      - name: Test with race detector
        run: go test -race -count=1 -short ./...

      - name: Generate coverage
        run: |
          go test -coverprofile=coverage.out -covermode=atomic ./...
          go tool cover -func=coverage.out

      - name: Upload coverage
        uses: codecov/codecov-action@v4
        with:
          files: ./coverage.out

Test-Suite-Integrationsbeispiel

package integration_test

import (
    "context"
    "testing"

    "github.com/stretchr/testify/suite"
    "myapp/model"
    "myapp/repository"
    "myapp/testutil"
)

type IntegrationSuite struct {
    suite.Suite
    db    *testutil.TestDB
    repos *repository.Repositories
}

func (s *IntegrationSuite) SetupSuite() {
    s.db = testutil.NewTestDB()
    s.db.Migrate()
    s.repos = repository.NewRepositories(s.db.Conn())
}

func (s *IntegrationSuite) TearDownSuite() {
    s.db.Close()
}

func (s *IntegrationSuite) SetupTest() {
    s.db.TruncateTables()
}

func (s *IntegrationSuite) TestUserCRUD() {
    ctx := context.Background()

    user := &model.User{Name: "Alice", Email: "alice@example.com"}
    s.NoError(s.repos.User.Create(ctx, user))
    s.NotZero(user.ID)

    found, err := s.repos.User.GetByID(ctx, user.ID)
    s.NoError(err)
    s.Equal("Alice", found.Name)

    found.Name = "Bob"
    s.NoError(s.repos.User.Update(ctx, found))

    updated, _ := s.repos.User.GetByID(ctx, user.ID)
    s.Equal("Bob", updated.Name)

    s.NoError(s.repos.User.Delete(ctx, user.ID))
    _, err = s.repos.User.GetByID(ctx, user.ID)
    s.Error(err)
}

func TestIntegrationSuite(t *testing.T) {
    suite.Run(t, new(IntegrationSuite))
}

5 Häufige Fallstricke

Fallstrick 1: require in tabellengesteuerten Tests überspringt nachfolgende Assertionen

Falsch:

for _, tt := range tests {
    t.Run(tt.name, func(t *testing.T) {
        result, err := DoSomething(tt.input)
        require.NoError(t, err) // Skips subsequent assertions on failure
        require.Equal(t, tt.want, result)
    })
}

Richtig:

for _, tt := range tests {
    t.Run(tt.name, func(t *testing.T) {
        result, err := DoSomething(tt.input)
        assert.NoError(t, err)
        assert.Equal(t, tt.want, result)
    })
}

Hinweis: t.Run erstellt unabhängige Subtests. require beendet nur den Subtest, in dem es sich befindet, nicht andere Fälle. Aber innerhalb eines einzelnen Subtests, wenn Sie alle Assertionsergebnisse sehen möchten, verwenden Sie assert.

Fallstrick 2: AssertExpectations-Aufruf vergessen

Falsch:

func TestWithMock(t *testing.T) {
    m := new(MockRepo)
    m.On("Get", mock.Anything, 1).Return(nil, nil)
    // No AssertExpectations!
}

Richtig:

func TestWithMock(t *testing.T) {
    m := new(MockRepo)
    m.On("Get", mock.Anything, 1).Return(nil, nil).Once()
    // ... use m
    m.AssertExpectations(t)
}

Fallstrick 3: Mock-Rückgabetyp-Fehlanpassung verursacht Panic

Falsch:

m.On("GetByID", mock.Anything, int64(1)).Return(nil, nil)
// nil type ambiguity, may cause panic

Richtig:

m.On("GetByID", mock.Anything, int64(1)).Return(&model.User{ID: 1}, nil)
// Explicitly return concrete type values

Fallstrick 4: t.Run direkt innerhalb von suite verwenden

Falsch:

func (s *MySuite) TestSomething() {
    s.T().Run("subtest", func(t *testing.T) {
        s.Equal(1, 2) // s.T() may point to wrong t
    })
}

Richtig:

func (s *MySuite) TestSomething() {
    s.Equal(1, 1)
    s.NoError(nil)
}

// Or split into separate test methods
func (s *MySuite) TestSomethingCase1() {
    s.Equal(1, 1)
}

func (s *MySuite) TestSomethingCase2() {
    s.Equal(2, 2)
}

Fallstrick 5: Content-Type in httptest vergessen

Falsch:

req, _ := http.NewRequest("POST", "/api/users", strings.NewReader(body))
// No Content-Type set
router.ServeHTTP(w, req)

Richtig:

req, _ := http.NewRequest("POST", "/api/users", strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)

Fehlerbehebungsreferenz

Fehlermeldung Ursache Lösung
assert: arguments: Call may need to use WithArgs Mock On-Methoden-Parametertypen stimmen nicht mit dem tatsächlichen Aufruf überein Sicherstellen, dass On-Parametertypen mit tatsächlichen Aufrufen übereinstimmen, mock.Anything für beliebige Parameter verwenden
mock: Unexpected Method Call Eine Mock-Methode aufgerufen, die nicht über On registriert wurde Prüfen, ob On-Registrierung fehlt, oder .Maybe() für optionale Aufrufe hinzufügen
panic: interface conversion: interface {} is nil, not *X Mock Return nil kann nicht in konkreten Typ konvertiert werden Return(&X{}, nil) statt Return(nil, nil) verwenden
suite: test method should have signature TestXxx(t *testing.T) Suite-Einstiegsfunktion-Signaturfehler Sicherstellen, dass die Einstiegsfunktion func TestXxxSuite(t *testing.T) ist und suite.Run aufruft
cannot use m (type *MockRepo) as type Repo in assignment Mock-Struktur implementiert Interface nicht vollständig mockgen-Tool zur automatischen Mock-Generierung verwenden, sicherstellen, dass alle Interface-Methoden implementiert sind
test timed out after 10m0s Mock-Methode blockiert, wartet aber wird nie aufgerufen Prüfen, ob die Geschäftslogik die Mock-Methode korrekt aufruft, oder .Maybe() hinzufügen
race detected during execution of test Nebenläufiger Datenwettlauf im Test Tests mit -race-Flag ausführen, Zugriff auf geteilten Zustand prüfen
testify: AssertExpectations failed Registrierte Mock-Methode wurde nie aufgerufen Prüfen, ob die Geschäftslogik den Methodenaufruf verpasst hat, oder .Maybe() für optionale Aufrufe verwenden
invalid argument for Int method Mock-Parameter verwendete falschen Typ int64(1) statt 1 verwenden, sicherstellen, dass Parametertypen mit Interface-Signatur übereinstimmen
go: module github.com/stretchr/testify not found testify-Abhängigkeit nicht installiert go get github.com/stretchr/testify@v1.9.0 ausführen

Erweiterte Optimierungstipps

Tipp 1: Benutzerdefinierte Assertionen zur Reduzierung von Wiederholungscode

package testutil

import (
    "testing"
    "time"

    "github.com/stretchr/testify/assert"
    "myapp/model"
)

// AssertUserEqual custom user assertion, ignoring ID and CreatedAt
func AssertUserEqual(t *testing.T, expected, actual *model.User, msgAndArgs ...interface{}) {
    t.Helper()
    assert.Equal(t, expected.Name, actual.Name, msgAndArgs...)
    assert.Equal(t, expected.Email, actual.Email, msgAndArgs...)
    assert.Equal(t, expected.Age, actual.Age, msgAndArgs...)
    if expected.Status != "" {
        assert.Equal(t, expected.Status, actual.Status, msgAndArgs...)
    }
}

// AssertTimeApproximately time proximity assertion
func AssertTimeApproximately(t *testing.T, expected, actual time.Time, delta time.Duration, msgAndArgs ...interface{}) {
    t.Helper()
    assert.WithinDuration(t, expected, actual, delta, msgAndArgs...)
}

Tipp 2: Mocks automatisch mit mockgen generieren

# Install mockgen
go install go.uber.org/mock/mockgen@latest

# Generate Mock for interface
mockgen -source=repository.go -destination=mock_repository.go -package=repository_test

# Use comment directive for auto-generation
//go:generate mockgen -source=repository.go -destination=mock_repository.go -package=repository_test

Tipp 3: Test-Teilmengenausführung mit Build-Tags

// Use build tag to control integration tests
//go:build integration

package integration_test

import "testing"

func TestDatabaseCRUD(t *testing.T) {
    // Only runs with go test -tags=integration
}
# Run unit tests only
go test -short ./...

# Run integration tests
go test -tags=integration ./...

Tipp 4: Parallele Testbeschleunigung

func TestParallel(t *testing.T) {
    t.Parallel()

    tests := []struct{ name string }{
        {name: "case1"},
        {name: "case2"},
    }

    for _, tt := range tests {
        tt := tt
        t.Run(tt.name, func(t *testing.T) {
            t.Parallel()
            // Test logic
        })
    }
}

Tipp 5: Golden-File-Testmuster

package handler_test

import (
    "context"
    "encoding/json"
    "os"
    "path/filepath"
    "testing"

    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"
)

func TestGetUserResponse(t *testing.T) {
    handler := setupHandler()
    resp := handler.GetUser(context.Background(), 1)

    actual, err := json.MarshalIndent(resp, "", "  ")
    require.NoError(t, err)

    goldenPath := filepath.Join("testdata", "get_user_response.json")

    if os.Getenv("UPDATE_GOLDEN") != "" {
        err = os.WriteFile(goldenPath, actual, 0644)
        require.NoError(t, err)
    }

    expected, err := os.ReadFile(goldenPath)
    require.NoError(t, err)
    assert.Equal(t, string(expected), string(actual))
}
# Update golden files
UPDATE_GOLDEN=1 go test ./handler/...

Test-Framework-Vergleich

Funktion testify gomock gocheck Standard testing
Assertionsreichtum ⭐⭐⭐⭐⭐ 100+ Methoden ⭐⭐ Grundlegende Assertionen ⭐⭐⭐ Ziemlich reichhaltig ⭐ Nur t.Error
Mock-Unterstützung ⭐⭐⭐⭐ Eingebautes mock-Paket ⭐⭐⭐⭐⭐ mockgen-Autogenerierung ⭐⭐ Nicht eingebaut ❌ Keine
Test-Suite ⭐⭐⭐⭐ suite-Paket ❌ Keine ⭐⭐⭐⭐⭐ Checker+Fixture ⭐ Nur Subtests
Lernkurve ⭐ Niedrig, intuitive API ⭐⭐⭐ Mittel, erfordert mockgen ⭐⭐⭐ Mittel ⭐ Am niedrigsten
Community-Aktivität ⭐⭐⭐⭐⭐ GitHub 23k+ Stars ⭐⭐⭐⭐ Uber-gepflegt ⭐⭐ Nischig ⭐⭐⭐⭐⭐ Offiziell
Tabellengesteuerte Kompatibilität ⭐⭐⭐⭐⭐ Perfekt ⭐⭐⭐ Anpassung erforderlich ⭐⭐⭐ Anpassung erforderlich ⭐⭐⭐⭐ Native Unterstützung
HTTP-Testing ⭐⭐⭐⭐ Mit httptest ⭐⭐⭐ Mit httptest ⭐⭐⭐ Mit httptest ⭐⭐⭐ Mit httptest
IDE-Unterstützung ⭐⭐⭐⭐⭐ Alle IDEs ⭐⭐⭐⭐ Alle IDEs ⭐⭐ Teilweise ⭐⭐⭐⭐⭐ Alle IDEs
Anwendungsfall Allgemeines Testen, schneller Einstieg Interface-Mock-lastige Projekte Komplexe Fixture-Projekte Einfache Projekte, Minimalismus

Zusammenfassung

Das Kernproblem beim Go-Testing ist nicht „wie man Assertionen schreibt", sondern „wie man Tests organisiert". assert und require lösen die Lesbarkeit von Assertionen, suite löst das Test-Lebenszyklusmanagement, mock löst die Abhängigkeitsisolation, httptest löst die HTTP-Testzuverlässigkeit und CI-Integration löst die Testkontinuität. Alle fünf zusammen bilden die vollständige Methodik für produktionsreifes Go-Testing. Denken Sie daran: Ein guter Test ist nicht einer, der umfangreich geschrieben wurde, sondern einer, der beim Scheitern Probleme präzise eingrenzt.


Verwandte Werkzeuge

  • JSON Formatter — API-Antwort-JSON formatieren, Datenstrukturen in Test-Assertionen schnell verifizieren
  • Hash Calculator — Anfragesignaturen und Datenprüfsummen berechnen, Mock-Datenkonsistenz sicherstellen
  • cURL to Code — cURL-Befehle in Go-HTTP-Testcode konvertieren, Handler-Testentwicklung beschleunigen

Probiere diese browser-lokalen Tools aus — keine Registrierung erforderlich →

#Go测试#testify#Go单元测试#表驱动测试#2026#编程语言