Testar código é essencial para garantir qualidade, confiança e manutenibilidade de software. Go possui uma biblioteca de testes robusta e bem integrada na linguagem. Neste guia completo, vamos explorar desde testes unitários básicos até TDD, mocks, benchmarks e testes de integração.

Por Que Testar em Go?

Go torna testes simples com:

  • Package testing embutido - sem dependências externas
  • Testes compilados - rápidos e eficientes
  • Coverage nativo - go test -cover
  • Benchmarks - testes de performance integrados
  • Paralelismo - t.Parallel() para testes concorrentes

Testes Unitários Básicos

Arquivos de teste terminam com _test.go:

// calculator.go
package calculator

func Add(a, b int) int {
    return a + b
}

func Divide(a, b int) (int, error) {
    if b == 0 {
        return 0, errors.New("divisão por zero")
    }
    return a / b, nil
}
// calculator_test.go
package calculator

import "testing"

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

func TestDivideByZero(t *testing.T) {
    _, err := Divide(10, 0)
    if err == nil {
        t.Error("esperava erro para divisão por zero")
    }
}

Execute com go test -v para ver detalhes.

Table-Driven Tests

O padrão mais idiomático em Go:

func TestAdd(t *testing.T) {
    tests := []struct {
        name     string
        a, b     int
        expected int
    }{
        {"positivos", 2, 3, 5},
        {"negativos", -2, -3, -5},
        {"zero", 0, 5, 5},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result := Add(tt.a, tt.b)
            if result != tt.expected {
                t.Errorf("Add(%d, %d) = %d; esperado %d",
                    tt.a, tt.b, result, tt.expected)
            }
        })
    }
}

Usando testify

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

func TestWithTestify(t *testing.T) {
    // assert continua após falha
    assert.Equal(t, 5, Add(2, 3))
    
    // require para imediatamente
    require.NoError(t, err)
    require.NotNil(t, user)
}

assert vs require

  • assert: Loga erro mas continua o teste
  • require: Para o teste imediatamente na falha
func TestUser(t *testing.T) {
    user, err := CreateUser("joao@example.com")
    
    require.NoError(t, err)      // Para se erro
    require.NotNil(t, user)      // Para se nil
    assert.Equal(t, "João", user.Name)  // Seguro
}

Mocking

Interface para Mock

type UserRepository interface {
    GetByID(id string) (*User, error)
    Save(user *User) error
}

Hand Mock (Sem Framework)

type MockUserRepository struct {
    GetByIDFunc func(id string) (*User, error)
    SaveFunc    func(user *User) error
}

func (m *MockUserRepository) GetByID(id string) (*User, error) {
    if m.GetByIDFunc != nil {
        return m.GetByIDFunc(id)
    }
    return nil, nil
}

Testando com Mock

func TestService_GetUser(t *testing.T) {
    mock := &MockUserRepository{
        GetByIDFunc: func(id string) (*User, error) {
            return &User{ID: id, Name: "João"}, nil
        },
    }
    
    svc := NewService(mock)
    user, err := svc.GetUser("123")
    
    require.NoError(t, err)
    assert.Equal(t, "João", user.Name)
}

Testes de HTTP

func TestGetUsers(t *testing.T) {
    req, err := http.NewRequest("GET", "/api/users", nil)
    require.NoError(t, err)

    rr := httptest.NewRecorder()
    handler := http.HandlerFunc(getUsersHandler)
    handler.ServeHTTP(rr, req)

    assert.Equal(t, http.StatusOK, rr.Code)
    
    var users []User
    json.Unmarshal(rr.Body.Bytes(), &users)
    assert.Len(t, users, 2)
}

Benchmarks

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

// Com memória
func BenchmarkStringConcat(b *testing.B) {
    b.Run("naive", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            var s string
            for j := 0; j < 100; j++ {
                s += "a"
            }
        }
    })
    
    b.Run("builder", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            var builder strings.Builder
            for j := 0; j < 100; j++ {
                builder.WriteString("a")
            }
        }
    })
}
go test -bench=. -benchmem

TDD - Test Driven Development

Ciclo Red-Green-Refactor:

  1. RED: Escreva teste que falha
  2. GREEN: Código mínimo para passar
  3. REFACTOR: Melhore mantendo testes verdes
// Teste primeiro (falha)
func TestValidator(t *testing.T) {
    v := NewValidator(WithMinLength(8))
    assert.Error(t, v.Validate("curta"))
    assert.NoError(t, v.Validate("senhaok123"))
}

// Implementação mínima
func (v *Validator) Validate(p string) error {
    if len(p) < v.minLength {
        return errors.New("curta")
    }
    return nil
}

// Refatoração
func (v *Validator) Validate(p string) error {
    if len(p) < v.minLength {
        return fmt.Errorf("mínimo %d caracteres", v.minLength)
    }
    return nil
}

Coverage

# Ver cobertura
go test -cover ./...

# Gerar relatório
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out

Comandos Úteis

# Executar todos os testes
go test ./...

# Verbose
go test -v ./...

# Testes específicos
go test -run TestNome ./...

# Paralelo
go test -parallel 4 ./...

# Tags (testes de integração)
go test -tags=integration ./...

Checklist de Qualidade

  • Cobertura > 80%
  • Table-driven tests
  • Testes para erros
  • Paralelismo onde possível
  • Cleanup com t.Cleanup()
  • Benchmarks para código crítico

Próximos Passos


Testes garantem confiança no código. Invista neles!