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 testFALHA (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 testPASSA

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étricaMetaFerramenta
Cobertura> 80%go test -cover
Lint0 errosgolangci-lint
Vulnerabilidades0 críticasgovulncheck
Build Time< 5 minGitHub Actions
Test Duration< 2 mingo test

Próximos Passos


TDD e CI/CD garantem código confiável e entrega contínua. Implemente hoje!