M02·07Testes automatizados

CAPÍTULO 07

Testes automatizados

Testes unitários, mocks com interfaces e integração com banco real — a pirâmide de testes em Go.

Por Thiago Souza9 min de leituraAtualizado em 2026-05

Analogia do controle de qualidade: numa fábrica de carros, você não testa um carro pronto rodando na rua para descobrir que o freio falhou. Você testa o freio sozinho, o motor sozinho, as luzes sozinhas. E só depois testa o carro inteiro.

No backend é igual:

  • Teste unitário — uma função/método sozinho.
  • Teste de integração — várias peças juntas (service + banco real, por exemplo).
  • Teste end-to-end — sistema todo, do HTTP ao banco.

A pirâmide de testes diz: muitos unitários, alguns de integração, pouquíssimos e2e. Eles ficam mais lentos e frágeis quanto mais altos.

Testes unitários em Go

Go tem teste embutido. Não precisa de framework. Arquivo foo_test.go ao lado de foo.go:

go
// internal/user/service_test.go
package user_test
 
import (
    "context"
    "errors"
    "testing"
    "meuprojeto/internal/user"
)
 
func TestCreateUser_InvalidEmail(t *testing.T) {
    svc := user.NewService(nil)  // nem precisa de repo: vai falhar antes
 
    _, err := svc.CreateUser(context.Background(), user.CreateUserInput{
        Email:    "isso-nao-eh-email",
        Password: "senha123",
    })
 
    if !errors.Is(err, user.ErrInvalidEmail) {
        t.Fatalf("esperava ErrInvalidEmail, recebi: %v", err)
    }
}

Roda com go test ./.... Fim.

Mocks (testes com fingidos)

Analogia do dublê de cinema: quando o ator não pode fazer a cena perigosa, entra um dublê. No teste, quando o componente real é pesado (banco, API externa), entra um mock — um substituto controlado.

Em Go, mocks bons se aproveitam de interfaces. Por isso a gente insistiu tanto nelas lá em cima. Vamos testar o UserService sem subir banco:

go
package user_test
 
import (
    "context"
    "testing"
    "meuprojeto/internal/user"
)
 
// mock manual (em Go, mock manual é frequentemente mais simples e claro
// do que gerar com mockgen, especialmente quando a interface é pequena).
type fakeRepo struct {
    saveFn         func(ctx context.Context, u user.User) (user.User, error)
    findByEmailFn  func(ctx context.Context, email string) (user.User, error)
}
 
func (f *fakeRepo) Save(ctx context.Context, u user.User) (user.User, error) {
    return f.saveFn(ctx, u)
}
func (f *fakeRepo) FindByEmail(ctx context.Context, email string) (user.User, error) {
    return f.findByEmailFn(ctx, email)
}
 
func TestCreateUser_Success(t *testing.T) {
    repo := &fakeRepo{
        // simulamos que ninguém com esse email existe ainda
        findByEmailFn: func(_ context.Context, _ string) (user.User, error) {
            return user.User{}, user.ErrNotFound
        },
        // simulamos que o save funciona e devolve com ID 42
        saveFn: func(_ context.Context, u user.User) (user.User, error) {
            u.ID = 42
            return u, nil
        },
    }
 
    svc := user.NewService(repo)
 
    u, err := svc.CreateUser(context.Background(), user.CreateUserInput{
        Email:    "ana@exemplo.com",
        Password: "umaSenhaForte!123",
    })
 
    if err != nil {
        t.Fatalf("não esperava erro, mas veio: %v", err)
    }
    if u.ID != 42 {
        t.Fatalf("esperava ID 42, veio %d", u.ID)
    }
}

Bibliotecas que ajudam:

  • testify (assert, require) — asserções mais legíveis.
  • mockgen (Uber gomock) — gera mocks automaticamente a partir de interfaces.
  • testcontainers-go — sobe Postgres/Redis de verdade dentro de Docker durante os testes de integração. Excelente para testar repositórios sem mockar SQL.