Golang Testing

作者 affaan-mef648e01899b無授權條款275K 個星標收錄於 2026年10月8日更新於 2026年10月8日儲存庫3 天前更新

Patrones de pruebas Go incluyendo pruebas basadas en tablas, subpruebas, benchmarks, fuzzing y cobertura de código. Sigue la metodología TDD con prácticas idiomáticas de Go.

AI 產生的概覽

Go 測試模式,涵蓋表格驅動測試、子測試、基準測試、模糊測試、模擬與程式碼覆蓋率。

功能
此技能提供 Go 測試模式與指引,包括表格驅動測試、子測試與平行子測試、測試輔助函式、golden 檔案、以介面為基礎的模擬、基準測試、模糊測試和程式碼覆蓋率。它也說明 TDD 紅-綠-重構工作流程,並列出常用的 go test 指令。它僅為說明性內容,不會產生檔案或指令碼。
適用情境
適用於撰寫新的 Go 函式或方法、為既有 Go 程式碼補充測試覆蓋,或建立基準測試與模糊測試時。也適合在 Go 專案中遵循 TDD 工作流程的團隊。
執行需求
需要 Go 工具鏈(模糊測試需 Go 1.18 以上)。不隨附指令碼,僅為說明性內容。

Patrones de Pruebas Go

Patrones completos de pruebas Go para escribir pruebas confiables y mantenibles siguiendo la metodología TDD.

Cuándo Activar

  • Escribir nuevas funciones o métodos Go
  • Agregar cobertura de pruebas a código existente
  • Crear benchmarks para código crítico en rendimiento
  • Implementar pruebas fuzz para validación de entradas
  • Seguir el flujo de trabajo TDD en proyectos Go

Flujo de Trabajo TDD para Go

El Ciclo RED-GREEN-REFACTOR

RED     → Escribir una prueba que falle primeroGREEN   → Escribir el código mínimo para pasar la pruebaREFACTOR → Mejorar el código manteniendo las pruebas en verdeREPEAT  → Continuar con el siguiente requisito

TDD Paso a Paso en Go

go
// Paso 1: Definir la interfaz/firma// calculator.gopackage calculator
func Add(a, b int) int {    panic("not implemented") // Marcador de posición}
// Paso 2: Escribir prueba que falle (RED)// calculator_test.gopackage calculator
import "testing"
func TestAdd(t *testing.T) {    got := Add(2, 3)    want := 5    if got != want {        t.Errorf("Add(2, 3) = %d; want %d", got, want)    }}
// Paso 3: Ejecutar prueba - verificar FALLO// $ go test// --- FAIL: TestAdd (0.00s)// panic: not implemented
// Paso 4: Implementar código mínimo (GREEN)func Add(a, b int) int {    return a + b}
// Paso 5: Ejecutar prueba - verificar PASA// $ go test// PASS
// Paso 6: Refactorizar si es necesario, verificar que las pruebas sigan pasando

Pruebas Basadas en Tablas

El patrón estándar para pruebas Go. Permite cobertura comprensiva con código mínimo.

go
func TestAdd(t *testing.T) {    tests := []struct {        name     string        a, b     int        expected int    }{        {"positive numbers", 2, 3, 5},        {"negative numbers", -1, -2, -3},        {"zero values", 0, 0, 0},        {"mixed signs", -1, 1, 0},        {"large numbers", 1000000, 2000000, 3000000},    }
    for _, tt := range tests {        t.Run(tt.name, func(t *testing.T) {            got := Add(tt.a, tt.b)            if got != tt.expected {                t.Errorf("Add(%d, %d) = %d; want %d",                    tt.a, tt.b, got, tt.expected)            }        })    }}

Pruebas Basadas en Tablas con Casos de Error

go
func TestParseConfig(t *testing.T) {    tests := []struct {        name    string        input   string        want    *Config        wantErr bool    }{        {            name:  "valid config",            input: `{"host": "localhost", "port": 8080}`,            want:  &Config{Host: "localhost", Port: 8080},        },        {            name:    "invalid JSON",            input:   `{invalid}`,            wantErr: true,        },        {            name:    "empty input",            input:   "",            wantErr: true,        },        {            name:  "minimal config",            input: `{}`,            want:  &Config{}, // Valor cero de Config        },    }
    for _, tt := range tests {        t.Run(tt.name, func(t *testing.T) {            got, err := ParseConfig(tt.input)
            if tt.wantErr {                if err == nil {                    t.Error("expected error, got nil")                }                return            }
            if err != nil {                t.Fatalf("unexpected error: %v", err)            }
            if !reflect.DeepEqual(got, tt.want) {                t.Errorf("got %+v; want %+v", got, tt.want)            }        })    }}

Subpruebas y Sub-benchmarks

Organizar Pruebas Relacionadas

go
func TestUser(t *testing.T) {    // Configuración compartida por todas las subpruebas    db := setupTestDB(t)
    t.Run("Create", func(t *testing.T) {        user := &User{Name: "Alice"}        err := db.CreateUser(user)        if err != nil {            t.Fatalf("CreateUser failed: %v", err)        }        if user.ID == "" {            t.Error("expected user ID to be set")        }    })
    t.Run("Get", func(t *testing.T) {        user, err := db.GetUser("alice-id")        if err != nil {            t.Fatalf("GetUser failed: %v", err)        }        if user.Name != "Alice" {            t.Errorf("got name %q; want %q", user.Name, "Alice")        }    })
    t.Run("Update", func(t *testing.T) {        // ...    })
    t.Run("Delete", func(t *testing.T) {        // ...    })}

Subpruebas en Paralelo

go
func TestParallel(t *testing.T) {    tests := []struct {        name  string        input string    }{        {"case1", "input1"},        {"case2", "input2"},        {"case3", "input3"},    }
    for _, tt := range tests {        tt := tt // Capturar variable del rango        t.Run(tt.name, func(t *testing.T) {            t.Parallel() // Ejecutar subpruebas en paralelo            result := Process(tt.input)            // aserciones...            _ = result        })    }}

Helpers de Prueba

Funciones Helper

go
func setupTestDB(t *testing.T) *sql.DB {    t.Helper() // Marca esta como función helper
    db, err := sql.Open("sqlite3", ":memory:")    if err != nil {        t.Fatalf("failed to open database: %v", err)    }
    // Limpieza cuando la prueba termina    t.Cleanup(func() {        db.Close()    })
    // Ejecutar migraciones    if _, err := db.Exec(schema); err != nil {        t.Fatalf("failed to create schema: %v", err)    }
    return db}
func assertNoError(t *testing.T, err error) {    t.Helper()    if err != nil {        t.Fatalf("unexpected error: %v", err)    }}
func assertEqual[T comparable](t *testing.T, got, want T) {    t.Helper()    if got != want {        t.Errorf("got %v; want %v", got, want)    }}

Archivos y Directorios Temporales

go
func TestFileProcessing(t *testing.T) {    // Crear directorio temporal - se limpia automáticamente    tmpDir := t.TempDir()
    // Crear archivo de prueba    testFile := filepath.Join(tmpDir, "test.txt")    err := os.WriteFile(testFile, []byte("test content"), 0644)    if err != nil {        t.Fatalf("failed to create test file: %v", err)    }
    // Ejecutar prueba    result, err := ProcessFile(testFile)    if err != nil {        t.Fatalf("ProcessFile failed: %v", err)    }
    // Aserciones...    _ = result}

Golden Files

Probar contra archivos de salida esperada almacenados en testdata/.

go
var update = flag.Bool("update", false, "update golden files")
func TestRender(t *testing.T) {    tests := []struct {        name  string        input Template    }{        {"simple", Template{Name: "test"}},        {"complex", Template{Name: "test", Items: []string{"a", "b"}}},    }
    for _, tt := range tests {        t.Run(tt.name, func(t *testing.T) {            got := Render(tt.input)
            golden := filepath.Join("testdata", tt.name+".golden")
            if *update {                // Actualizar golden file: go test -update                err := os.WriteFile(golden, got, 0644)                if err != nil {                    t.Fatalf("failed to update golden file: %v", err)                }            }
            want, err := os.ReadFile(golden)            if err != nil {                t.Fatalf("failed to read golden file: %v", err)            }
            if !bytes.Equal(got, want) {                t.Errorf("output mismatch:\ngot:\n%s\nwant:\n%s", got, want)            }        })    }}

Mocking con Interfaces

Mocking Basado en Interfaces

go
// Definir interfaz para dependenciastype UserRepository interface {    GetUser(id string) (*User, error)    SaveUser(user *User) error}
// Implementación de produccióntype PostgresUserRepository struct {    db *sql.DB}
func (r *PostgresUserRepository) GetUser(id string) (*User, error) {    // Consulta real a base de datos}
// Implementación mock para pruebastype MockUserRepository struct {    GetUserFunc  func(id string) (*User, error)    SaveUserFunc func(user *User) error}
func (m *MockUserRepository) GetUser(id string) (*User, error) {    return m.GetUserFunc(id)}
func (m *MockUserRepository) SaveUser(user *User) error {    return m.SaveUserFunc(user)}
// Prueba usando mockfunc TestUserService(t *testing.T) {    mock := &MockUserRepository{        GetUserFunc: func(id string) (*User, error) {            if id == "123" {                return &User{ID: "123", Name: "Alice"}, nil            }            return nil, ErrNotFound        },    }
    service := NewUserService(mock)
    user, err := service.GetUserProfile("123")    if err != nil {        t.Fatalf("unexpected error: %v", err)    }    if user.Name != "Alice" {        t.Errorf("got name %q; want %q", user.Name, "Alice")    }}

Benchmarks

Benchmarks Básicos

go
func BenchmarkProcess(b *testing.B) {    data := generateTestData(1000)    b.ResetTimer() // No contar el tiempo de configuración
    for i := 0; i < b.N; i++ {        Process(data)    }}
// Ejecutar: go test -bench=BenchmarkProcess -benchmem// Salida: BenchmarkProcess-8   10000   105234 ns/op   4096 B/op   10 allocs/op

Benchmark con Diferentes Tamaños

go
func BenchmarkSort(b *testing.B) {    sizes := []int{100, 1000, 10000, 100000}
    for _, size := range sizes {        b.Run(fmt.Sprintf("size=%d", size), func(b *testing.B) {            data := generateRandomSlice(size)            b.ResetTimer()
            for i := 0; i < b.N; i++ {                // Hacer una copia para evitar ordenar datos ya ordenados                tmp := make([]int, len(data))                copy(tmp, data)                sort.Ints(tmp)            }        })    }}

Benchmarks de Asignación de Memoria

go
func BenchmarkStringConcat(b *testing.B) {    parts := []string{"hello", "world", "foo", "bar", "baz"}
    b.Run("plus", func(b *testing.B) {        for i := 0; i < b.N; i++ {            var s string            for _, p := range parts {                s += p            }            _ = s        }    })
    b.Run("builder", func(b *testing.B) {        for i := 0; i < b.N; i++ {            var sb strings.Builder            for _, p := range parts {                sb.WriteString(p)            }            _ = sb.String()        }    })
    b.Run("join", func(b *testing.B) {        for i := 0; i < b.N; i++ {            _ = strings.Join(parts, "")        }    })}

Fuzzing (Go 1.18+)

Prueba Fuzz Básica

go
func FuzzParseJSON(f *testing.F) {    // Agregar corpus semilla    f.Add(`{"name": "test"}`)    f.Add(`{"count": 123}`)    f.Add(`[]`)    f.Add(`""`)
    f.Fuzz(func(t *testing.T, input string) {        var result map[string]interface{}        err := json.Unmarshal([]byte(input), &result)
        if err != nil {            // JSON inválido es esperado para entrada aleatoria            return        }
        // Si el parseo tuvo éxito, la re-codificación debería funcionar        _, err = json.Marshal(result)        if err != nil {            t.Errorf("Marshal failed after successful Unmarshal: %v", err)        }    })}
// Ejecutar: go test -fuzz=FuzzParseJSON -fuzztime=30s

Prueba Fuzz con Múltiples Entradas

go
func FuzzCompare(f *testing.F) {    f.Add("hello", "world")    f.Add("", "")    f.Add("abc", "abc")
    f.Fuzz(func(t *testing.T, a, b string) {        result := Compare(a, b)
        // Propiedad: Compare(a, a) siempre debe ser igual a 0        if a == b && result != 0 {            t.Errorf("Compare(%q, %q) = %d; want 0", a, b, result)        }
        // Propiedad: Compare(a, b) y Compare(b, a) deben tener signos opuestos        reverse := Compare(b, a)        if (result > 0 && reverse >= 0) || (result < 0 && reverse <= 0) {            if result != 0 || reverse != 0 {                t.Errorf("Compare(%q, %q) = %d, Compare(%q, %q) = %d; inconsistent",                    a, b, result, b, a, reverse)            }        }    })}

Cobertura de Código

Ejecutar Cobertura

bash
# Cobertura básicago test -cover ./...
# Generar perfil de coberturago test -coverprofile=coverage.out ./...
# Ver cobertura en el navegadorgo tool cover -html=coverage.out
# Ver cobertura por funcióngo tool cover -func=coverage.out
# Cobertura con detección de condiciones de carrerago test -race -coverprofile=coverage.out ./...

Objetivos de Cobertura

Tipo de CódigoObjetivo
Lógica de negocio crítica100%
APIs públicas90%+
Código general80%+
Código generadoExcluir

Excluir Código Generado de la Cobertura

go
//go:generate mockgen -source=interface.go -destination=mock_interface.go
// En el perfil de cobertura, excluir con build tags:// go test -cover -tags=!generate ./...

Pruebas de Handlers HTTP

go
func TestHealthHandler(t *testing.T) {    // Crear request    req := httptest.NewRequest(http.MethodGet, "/health", nil)    w := httptest.NewRecorder()
    // Llamar handler    HealthHandler(w, req)
    // Verificar respuesta    resp := w.Result()    defer resp.Body.Close()
    if resp.StatusCode != http.StatusOK {        t.Errorf("got status %d; want %d", resp.StatusCode, http.StatusOK)    }
    body, _ := io.ReadAll(resp.Body)    if string(body) != "OK" {        t.Errorf("got body %q; want %q", body, "OK")    }}
func TestAPIHandler(t *testing.T) {    tests := []struct {        name       string        method     string        path       string        body       string        wantStatus int        wantBody   string    }{        {            name:       "get user",            method:     http.MethodGet,            path:       "/users/123",            wantStatus: http.StatusOK,            wantBody:   `{"id":"123","name":"Alice"}`,        },        {            name:       "not found",            method:     http.MethodGet,            path:       "/users/999",            wantStatus: http.StatusNotFound,        },        {            name:       "create user",            method:     http.MethodPost,            path:       "/users",            body:       `{"name":"Bob"}`,            wantStatus: http.StatusCreated,        },    }
    handler := NewAPIHandler()
    for _, tt := range tests {        t.Run(tt.name, func(t *testing.T) {            var body io.Reader            if tt.body != "" {                body = strings.NewReader(tt.body)            }
            req := httptest.NewRequest(tt.method, tt.path, body)            req.Header.Set("Content-Type", "application/json")            w := httptest.NewRecorder()
            handler.ServeHTTP(w, req)
            if w.Code != tt.wantStatus {                t.Errorf("got status %d; want %d", w.Code, tt.wantStatus)            }
            if tt.wantBody != "" && w.Body.String() != tt.wantBody {                t.Errorf("got body %q; want %q", w.Body.String(), tt.wantBody)            }        })    }}

Comandos de Prueba

bash
# Ejecutar todas las pruebasgo test ./...
# Ejecutar pruebas con salida detalladago test -v ./...
# Ejecutar prueba específicago test -run TestAdd ./...
# Ejecutar pruebas que coincidan con patróngo test -run "TestUser/Create" ./...
# Ejecutar pruebas con detector de condiciones de carrerago test -race ./...
# Ejecutar pruebas con coberturago test -cover -coverprofile=coverage.out ./...
# Ejecutar solo pruebas cortasgo test -short ./...
# Ejecutar pruebas con timeoutgo test -timeout 30s ./...
# Ejecutar benchmarksgo test -bench=. -benchmem ./...
# Ejecutar fuzzinggo test -fuzz=FuzzParse -fuzztime=30s ./...
# Contar ejecuciones de prueba (para detección de pruebas inestables)go test -count=10 ./...

Buenas Prácticas

HACER:

  • Escribir pruebas PRIMERO (TDD)
  • Usar pruebas basadas en tablas para cobertura comprensiva
  • Probar comportamiento, no implementación
  • Usar t.Helper() en funciones helper
  • Usar t.Parallel() para pruebas independientes
  • Limpiar recursos con t.Cleanup()
  • Usar nombres de prueba significativos que describan el escenario

NO HACER:

  • Probar funciones privadas directamente (probar a través de la API pública)
  • Usar time.Sleep() en pruebas (usar canales o condiciones)
  • Ignorar pruebas inestables (corregirlas o eliminarlas)
  • Mockear todo (preferir pruebas de integración cuando sea posible)
  • Omitir pruebas de rutas de error

Integración con CI/CD

yaml
# Ejemplo de GitHub Actionstest:  runs-on: ubuntu-latest  steps:    - uses: actions/checkout@v4    - uses: actions/setup-go@v5      with:        go-version: '1.22'
    - name: Run tests      run: go test -race -coverprofile=coverage.out ./...
    - name: Check coverage      run: |        go tool cover -func=coverage.out | grep total | awk '{print $3}' | \        awk -F'%' '{if ($1 < 80) exit 1}'

Recuerda: Las pruebas son documentación. Muestran cómo se pretende usar tu código. Escríbelas con claridad y mantenlas actualizadas.

來源與署名

來源:affaan-m/ecc位於docs/es/skills/golang-testing提交ef648e0

授權條款: 無授權條款

內容歸原作者所有。SourceWeft 從公開儲存庫中收錄這些內容。

檢舉或申請下架