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.Runerstellt unabhängige Subtests.requirebeendet 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 Sieassert.
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 →