Janeiro 2026 · ~10 min

Go Testing: TDD e Integração Contínua - Guia Completo

Aprenda testes em Go com TDD, table-driven tests, cobertura, race detector, mocks simples e pipeline CI/CD com GitHub Actions para projetos profissionais.

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.

Em 2026, a pergunta prática para times brasileiros não é mais se vale testar Go, e sim qual pacote mínimo de qualidade impede regressão sem travar entrega. Vagas backend, plataforma e DevOps citam Go junto de GitHub Actions, Docker, Kubernetes, AWS, observabilidade e segurança; por isso um bom pipeline precisa provar mais do que go test feliz. Ele deve checar formatação, dependências, vulnerabilidades alcançáveis, corrida de dados, build e, quando fizer sentido, testes de integração.

Este tutorial foi atualizado para conectar TDD com produção: table-driven tests, go test -race, cobertura proporcional, govulncheck, cache de módulos no GitHub Actions e critérios de promoção para deploy. Se você ainda está montando a base da aplicação, combine este guia com API REST em Go, Go Modules na prática, autenticação e autorização em Go e GoReleaser com checksums e SBOM.

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

Pipeline mínimo para projeto Go em produção

Um pipeline bom começa pequeno e previsível. Para um serviço ou biblioteca Go que já recebe pull requests, o mínimo recomendável é:

  1. gofmt ou gofmt -w checado sem alterar arquivos no CI.
  2. go mod tidy validado para impedir dependência esquecida.
  3. go test ./... em todos os pacotes.
  4. go test -race ./... pelo menos em branches principais ou PRs relevantes.
  5. go vet ./... para erros comuns que o compilador não pega.
  6. govulncheck ./... quando o projeto expõe dependências a usuários ou produção.
  7. go build ./... ou build do binário principal.

A tentação é começar com um pipeline enorme. O melhor caminho é o oposto: crie um portão que rode sempre, passe rápido e bloqueie bugs reais. Depois adicione integração com banco, containers, cobertura mínima e release assinado conforme o risco do produto cresce.

Exemplo de GitHub Actions para Go

name: go-ci

on:
  pull_request:
  push:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-go@v5
        with:
          go-version: '1.25.x'
          cache: true

      - name: Check formatting
        run: |
          test -z "$(gofmt -l .)"

      - name: Check modules
        run: |
          go mod tidy
          git diff --exit-code go.mod go.sum

      - name: Vet
        run: go vet ./...

      - name: Tests
        run: go test ./...

      - name: Race detector
        run: go test -race ./...

      - name: Build
        run: go build ./...

Esse arquivo não faz deploy. Essa separação é saudável: primeiro prove que o código é confiável; depois promova artefato, imagem ou binário. Para binários públicos, conecte com GoReleaser. Para APIs, conecte com Docker, health checks e graceful shutdown.

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

Checklist de maturidade para times Go

Antes de chamar um projeto Go de “pronto para produção”, revise estes pontos:

  • O CI roda em todo pull request e em push para main.
  • Falha de teste, formatação, go vet ou go mod tidy bloqueia merge.
  • Testes unitários cobrem regra de negócio, não só handlers felizes.
  • Testes de integração usam banco/serviço realista quando contrato externo importa.
  • O pipeline usa cache, mas não depende de estado local escondido.
  • Vulnerabilidades são triadas com contexto, não ignoradas nem aceitas cegamente.
  • O release gera artefato rastreável: commit, versão, imagem ou binário.
  • Deploy tem rollback, health check e logs suficientes para diagnosticar regressão.

Para carreira, esse checklist vira argumento em entrevista. Em vez de dizer apenas “sei testes”, você consegue explicar como evitar regressão, como organizar table-driven tests, quando usar race detector, como colocar govulncheck no CI e como separar CI de deploy. Compare com entrevista técnica Go e com vagas Go DevOps/SRE para ver como esses sinais aparecem nas descrições reais.

Para comparar como esse mesmo tema aparece em outras stacks do mercado brasileiro, acompanhe também Python Dev Brasil. Testes, CI e deploy mudam de ferramenta, mas a lógica de portão de qualidade, revisão e observabilidade é a mesma.


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