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
testingembutido - 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 testerequire: 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:
- RED: Escreva teste que falha
- GREEN: Código mínimo para passar
- 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
- Go para APIs REST — APIs com testes
- Go Concurrency — Testes concorrentes
Testes garantem confiança no código. Invista neles!