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:
// 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:
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.