Test-Driven Development (TDD) e Integração Contínua (CI/CD) são práticas essenciais para desenvolvimento de software profissional. Este guia completo mostra como implementar TDD e pipelines de CI/CD em projetos Go, desde testes unitários até deploy automatizado.
Por Que TDD + CI/CD em Go?
Go é ideal para TDD e CI/CD porque:
- Testes rápidos — Compilação e execução em segundos
- Binário único — Deploy simplificado
- Biblioteca padrão robusta — Sem dependências complexas
- Cross-compilation nativa — Build para múltiplas plataformas
Fundamentos de TDD
O Ciclo Red-Green-Refactor
┌─────────┐ ┌─────────┐ ┌─────────┐
│ RED │ → │ GREEN │ → │ REFACTOR│
│ (Falha)│ │ (Passa) │ │(Melhora)│
└─────────┘ └─────────┘ └─────────┘
↑ │
└──────────────────────────────┘
Exemplo Prático: Calculadora com TDD
Passo 1: Escreva o teste (RED)
// calculator_test.go
package calculator
import "testing"
func TestAdd(t *testing.T) {
calc := NewCalculator()
result := calc.Add(2, 3)
if result != 5 {
t.Errorf("Add(2, 3) = %d; esperado 5", result)
}
}
Execute: go test — FALHA (código não existe)
Passo 2: Implementação mínima (GREEN)
// calculator.go
package calculator
type Calculator struct{}
func NewCalculator() *Calculator {
return &Calculator{}
}
func (c *Calculator) Add(a, b int) int {
return a + b
}
Execute: go test — PASSA ✓
Passo 3: Refatoração
// Adicione mais casos de teste
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},
{"ambos_zero", 0, 0, 0},
}
calc := NewCalculator()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := calc.Add(tt.a, tt.b)
if got != tt.expected {
t.Errorf("Add(%d, %d) = %d; esperado %d",
tt.a, tt.b, got, tt.expected)
}
})
}
}
Código Testável em Go
Princípios SOLID para Testes
1. Inversão de Dependência (DIP)
// ❌ Ruim: Acoplamento direto
type Service struct {
db *sql.DB // dependência concreta
}
// ✅ Bom: Dependa de interfaces
type Repository interface {
GetUser(id string) (*User, error)
SaveUser(user *User) error
}
type Service struct {
repo Repository // dependência abstrata
}
func NewService(repo Repository) *Service {
return &Service{repo: repo}
}
2. Injeção de Dependências
// main.go - Wiring manual
func main() {
db, _ := sql.Open("postgres", dsn)
repo := NewPostgresRepository(db)
service := NewService(repo)
handler := NewHandler(service)
// ...
}
Mocking e Stubs
Mock Manual
// Mock para testes
type MockRepository struct {
users map[string]*User
err error
}
func (m *MockRepository) GetUser(id string) (*User, error) {
if m.err != nil {
return nil, m.err
}
return m.users[id], nil
}
func (m *MockRepository) SaveUser(user *User) error {
if m.err != nil {
return m.err
}
m.users[user.ID] = user
return nil
}
Teste com Mock
func TestService_GetUser(t *testing.T) {
// Setup
mockRepo := &MockRepository{
users: map[string]*User{
"123": {ID: "123", Name: "João"},
},
}
svc := NewService(mockRepo)
// Execute
user, err := svc.GetUser("123")
// Assert
if err != nil {
t.Errorf("erro inesperado: %v", err)
}
if user.Name != "João" {
t.Errorf("nome = %s; esperado João", user.Name)
}
}
func TestService_GetUser_NotFound(t *testing.T) {
mockRepo := &MockRepository{users: map[string]*User{}}
svc := NewService(mockRepo)
_, err := svc.GetUser("999")
if err != ErrUserNotFound {
t.Errorf("erro = %v; esperado ErrUserNotFound", err)
}
}
Análise de Cobertura
Gerar Relatório de Coverage
# Cobertura do pacote atual
go test -cover ./...
# Cobertura detalhada
go test -coverprofile=coverage.out ./...
# Visualizar em HTML
go tool cover -html=coverage.out -o coverage.html
# Ver funções não cobertas
go tool cover -func=coverage.out
Cobertura Mínima em CI
#!/bin/bash
# check-coverage.sh
THRESHOLD=80
coverage=$(go test -cover ./... | grep -o '[0-9.]*%' | tr -d '%' | awk '{s+=$1; n++} END {printf "%.2f", s/n}')
echo "Cobertura: $coverage%"
if (( $(echo "$coverage < $THRESHOLD" | bc -l) )); then
echo "❌ Cobertura abaixo de $THRESHOLD%"
exit 1
fi
echo "✅ Cobertura aceita"
GitHub Actions para Go
Pipeline Básica
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.22'
cache: true
- name: Download dependencies
run: go mod download
- name: Run tests
run: go test -v -race ./...
- name: Check coverage
run: |
go test -coverprofile=coverage.out ./...
go tool cover -func=coverage.out
Pipeline Avançada
# .github/workflows/ci-cd.yml
name: CI/CD Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
GO_VERSION: '1.22'
REGISTRY: ghcr.io
jobs:
# Job 1: Lint e Testes
test:
name: Testes
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
- name: Cache Go modules
uses: actions/cache@v4
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
- name: Download dependencies
run: go mod download
- name: Run linter
uses: golangci/golangci-lint-action@v3
with:
version: latest
args: --timeout=5m
- name: Run tests
run: go test -v -race -coverprofile=coverage.out ./...
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
file: ./coverage.out
fail_ci_if_error: false
# Job 2: Build
build:
name: Build
runs-on: ubuntu-latest
needs: test
strategy:
matrix:
os: [linux, darwin, windows]
arch: [amd64, arm64]
exclude:
- os: windows
arch: arm64
steps:
- uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
- name: Build binary
env:
GOOS: ${{ matrix.os }}
GOARCH: ${{ matrix.arch }}
run: |
output_name="myapp-${{ matrix.os }}-${{ matrix.arch }}"
if [ "$GOOS" = "windows" ]; then
output_name+='.exe'
fi
go build -ldflags="-s -w" -o "dist/$output_name" ./cmd/app
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: binaries
path: dist/
# Job 3: Deploy (somente na main)
deploy:
name: Deploy
runs-on: ubuntu-latest
needs: [test, build]
if: github.ref == 'refs/heads/main'
steps:
- name: Download artifacts
uses: actions/download-artifact@v4
with:
name: binaries
path: dist/
- name: Deploy to staging
run: |
echo "Deploy para staging..."
# Comandos de deploy aqui
Benchmarks em CI
Detectar Regressões de Performance
- name: Run benchmarks
run: |
go test -bench=. -benchmem ./... | tee benchmark.txt
- name: Compare benchmarks
uses: benchmark-action/github-action-benchmark@v1
with:
tool: 'go'
output-file-path: benchmark.txt
github-token: ${{ secrets.GITHUB_TOKEN }}
auto-push: true
Benchmark Script
// Exemplo de benchmark para CI
func BenchmarkProcessData(b *testing.B) {
data := generateTestData(1000)
b.ResetTimer()
for i := 0; i < b.N; i++ {
ProcessData(data)
}
}
func BenchmarkProcessDataParallel(b *testing.B) {
data := generateTestData(1000)
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
ProcessData(data)
}
})
}
Testes de Integração
Estratégia de Testes
unit/
├── service_test.go # Testes unitários (mock)
└── handler_test.go # Testes HTTP (httptest)
integration/
├── database_test.go # Testes com banco real
└── api_test.go # Testes end-to-end
Test Container para PostgreSQL
// integration_test.go
//go:build integration
package integration
import (
"context"
"testing"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/modules/postgres"
)
func TestDatabaseIntegration(t *testing.T) {
ctx := context.Background()
// Criar container PostgreSQL
container, err := postgres.Run(ctx,
"postgres:16",
postgres.WithDatabase("testdb"),
postgres.WithUsername("test"),
postgres.WithPassword("test"),
)
if err != nil {
t.Fatal(err)
}
defer container.Terminate(ctx)
// Obter connection string
connStr, _ := container.ConnectionString(ctx)
// Testar com banco real
db, err := sql.Open("postgres", connStr)
if err != nil {
t.Fatal(err)
}
// Executar testes...
}
Execute: go test -tags=integration ./integration/
Projeto Real: Estrutura Completa
myproject/
├── .github/
│ └── workflows/
│ ├── ci.yml # Testes e lint
│ └── release.yml # Build e release
├── cmd/
│ └── api/
│ └── main.go
├── internal/
│ ├── domain/
│ │ ├── user.go
│ │ └── user_test.go
│ ├── service/
│ │ ├── user_service.go
│ │ └── user_service_test.go
│ └── repository/
│ ├── postgres/
│ │ ├── user_repo.go
│ │ └── user_repo_test.go
│ └── mock/
│ └── user_repo_mock.go
├── pkg/
│ └── utils/
│ └── validator.go
├── Makefile
├── go.mod
└── README.md
Makefile Útil
.PHONY: test coverage lint build clean
test:
go test -v -race ./...
coverage:
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out
lint:
golangci-lint run
build:
go build -ldflags="-s -w" -o bin/api ./cmd/api
clean:
rm -rf bin/ coverage.out
integration-test:
go test -tags=integration ./integration/...
dev:
air
Checklist TDD + CI/CD
Antes de Commitar
- Todos os testes passam (
go test ./...) - Cobertura > 80%
- Linter sem erros (
golangci-lint run) - Código formatado (
go fmt ./...) - Módulos atualizados (
go mod tidy)
Pipeline CI/CD
- Build em múltiplas plataformas
- Testes unitários
- Testes de integração
- Análise de segurança (
govulncheck) - Verificação de cobertura
- Deploy automatizado
Métricas de Qualidade
Dashboard de Métricas
| Métrica | Meta | Ferramenta |
|---|---|---|
| Cobertura | > 80% | go test -cover |
| Lint | 0 erros | golangci-lint |
| Vulnerabilidades | 0 críticas | govulncheck |
| Build Time | < 5 min | GitHub Actions |
| Test Duration | < 2 min | go test |
Próximos Passos
- Go Clean Architecture — Estrutura testável
- Go Performance Profiling — Otimização com benchmarks
- GitHub Actions Docs — Automação avançada
TDD e CI/CD garantem código confiável e entrega contínua. Implemente hoje!