New to Claude Skills? Learn how to install them →

affaan-m on GitHub

Go Testing Patterns

Free

Implement TDD in Go with comprehensive testing patterns.

by affaan-m239.3k stars on affaan-m/ecc
2 views
Updated Aug 10, 2026
Get this skill

Free · Opens the source repo

What Go Testing Patterns does

The Go Testing Patterns skill provides a structured approach to writing reliable and maintainable tests in Go, adhering to Test-Driven Development (TDD) principles. It offers a variety of testing methodologies including table-driven tests, subtests, benchmarks, fuzz testing, and code coverage, making it a valuable resource for Go developers looking to enhance their testing practices. The skill emphasizes idiomatic Go practices, ensuring that tests are not only effective but also align with the language's conventions.

This skill is particularly useful when developing new functions or methods, as it guides users through the TDD cycle of RED-GREEN-REFACTOR. By following this cycle, developers can write failing tests first, implement the minimum code necessary to pass those tests, and then refactor the code while ensuring that all tests remain green. The included examples demonstrate how to set up tests, handle various input scenarios, and organize tests effectively, which helps in maintaining a clean and efficient codebase.

Additionally, the skill covers advanced testing techniques such as parallel subtests and the use of helper functions for common testing tasks. This not only streamlines the testing process but also encourages best practices in test organization and execution. With the ability to create benchmarks and perform fuzz testing, developers can ensure their code is robust against unexpected inputs and performs well under various conditions.

Overall, the Go Testing Patterns skill is an essential tool for Go developers who want to implement comprehensive testing strategies and improve the quality of their code through TDD practices.

When to use it

Use this skill when writing new Go functions or methods, adding test coverage to existing code, or when following TDD practices in your projects.

When not to use it

This skill may not be suitable for projects that do not require extensive testing or for developers who are not using Go as their primary programming language.

What you can build with it

Writing New Functions

When developing new functions in Go, use this skill to establish a TDD approach, ensuring reliability from the start.

Adding Test Coverage

If you have existing Go code without tests, leverage this skill to systematically add coverage and improve code quality.

Performance Benchmarking

Utilize the benchmarking patterns provided in this skill to measure and optimize the performance of critical code segments.

How to install Go Testing Patterns

View source

1. Install with the skills CLI

npx skills add affaan-m/ecc/golang-testing --agent claude-code

2. Or install it manually

Download the skill folder and drop it into ~/.claude/skills/ for all projects, or .claude/skills/ to scope it to one repo. Restart Claude Code so it picks up the new skill.

Anthropic's agentic coding CLI, and the reference implementation of Agent Skills. Drop a skill folder into ~/.claude/skills and Claude Code loads it automatically whenever a task matches the skill's description. Claude Code docs

Inside SKILL.md

Written by affaan-m

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 primero
GREEN   → Escribir el código mínimo para pasar la prueba
REFACTOR → Mejorar el código manteniendo las pruebas en verde
REPEAT  → Continuar con el siguiente requisito

TDD Paso a Paso en Go

// Paso 1: Definir la interfaz/firma
// calculator.go
package calculator

func Add(a, b int) int {
    panic("not implemented") // Marcador de posición
}

// Paso 2: Escribir prueba que falle (RED)
// calculator_test.go
package 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.

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

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

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

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

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

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/.

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

// Definir interfaz para dependencias
type UserRepository interface {
    GetUser(id string) (*User, error)
    SaveUser(user *User) error
}

// Implementación de producción
type PostgresUserRepository struct {
    db *sql.DB
}

func (r *PostgresUserRepository) GetUser(id string) (*User, error) {
    // Consulta real a base de datos
}

// Implementación mock para pruebas
type 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 mock
func 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

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

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

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

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

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

# Cobertura básica
go test -cover ./...

# Generar perfil de cobertura
go test -coverprofile=coverage.out ./...

# Ver cobertura en el navegador
go tool cover -html=coverage.out

# Ver cobertura por función
go tool cover -func=coverage.out

# Cobertura con detección de condiciones de carrera
go 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: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

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

# Ejecutar todas las pruebas
go test ./...

# Ejecutar pruebas con salida detallada
go test -v ./...

# Ejecutar prueba específica
go test -run TestAdd ./...

# Ejecutar pruebas que coincidan con patrón
go test -run "TestUser/Create" ./...

# Ejecutar pruebas con detector de condiciones de carrera
go test -race ./...

# Ejecutar pruebas con cobertura
go test -cover -coverprofile=coverage.out ./...

# Ejecutar solo pruebas cortas
go test -short ./...

# Ejecutar pruebas con timeout
go test -timeout 30s ./...

# Ejecutar benchmarks
go test -bench=. -benchmem ./...

# Ejecutar fuzzing
go 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

# Ejemplo de GitHub Actions
test:
  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.

Frequently asked questions about Go Testing Patterns

Similar skills