A pergunta mais comum de quem escreve o primeiro teste de serviço em Go não é “como faço assert”. É “como testo isso sem chamar o banco de verdade ou a API externa?”. É a pergunta certa: testes que dependem de PostgreSQL, Redis e SDK de nuvem são lentos, instáveis e falham por motivos que não têm a ver com a sua lógica. A resposta é a mesma que sustenta o resto do código idiomático: dependa de interfaces pequenas e troque a implementação no teste. Este guia mostra como fazer isso com httptest, fakes à mão, testify/mock e gomock, e quando cada um vale a pena em 2026.
Antes de continuar, vale uma verdade que economiza muito tempo: em Go, o “mock” muitas vezes é só um fake simples que você escreve em 15 linhas. Bibliotecas de mock existem e ajudam, mas a primeira opção deveria ser uma implementação em memória de uma interface. Se você ainda não está confortável com interfaces por trás das dependências, leia o guia de interfaces em Go e o de dependency injection sem framework — sem isso, mockar vira sofrimento.
Vocabulário: fake, stub, spy e mock
A comunidade de testes usa essas palavras com sentidos diferentes de fórum para fórum. Esta é a convenção que ajuda a decidir:
| Test double | O que faz | Quando usar |
|---|---|---|
| Stub | Devolve respostas fixas, ignora como foi chamado | Isolar uma entrada simples |
| Fake | Implementação real, porém simplificada (ex.: repositório em memória) | Testar lógica de domínio de ponta a ponta |
| Spy | Registra chamadas para você inspecionar depois | Verificar que algo foi chamado, sem bloquear |
| Mock | Vem com expectativas; falha se o comportamento não ocorrer | Garantir interações exatas com o colaborador |
A regra prática em Go: fake primeiro, mock depois. Fakes tornam o teste legível e resistente a refatoração; mocks deixam o teste frágil quando a implementação muda. Combine com testes de tabela para cobrir vários cenários com pouco código.
O pré-requisito: depender de interfaces
Para trocar uma dependência no teste, o consumidor precisa depender de uma interface, não de um tipo concreto. Um serviço que grava usuários não deveria saber que existe PostgreSQL:
package main
import (
"context"
"errors"
)
var ErrNaoEncontrado = errors.New("usuário não encontrado")
type Usuario struct {
ID int
Nome string
}
type RepositorioUsuario interface {
Salvar(ctx context.Context, u Usuario) error
Buscar(ctx context.Context, id int) (Usuario, error)
}
type Servico struct {
repo RepositorioUsuario
prox int
}
func NovoServico(repo RepositorioUsuario) *Servico {
return &Servico{repo: repo}
}
func (s *Servico) Criar(ctx context.Context, nome string) (Usuario, error) {
u := Usuario{ID: s.prox + 1, Nome: nome}
if err := s.repo.Salvar(ctx, u); err != nil {
return Usuario{}, err
}
s.prox = u.ID
return u, nil
}
A partir daqui, qualquer coisa que implemente Salvar e Buscar serve como dependência — inclusive um fake. Esse é o mesmo princípio que sustenta Clean Architecture em Go: a regra de negócio não conhece os detalhes de infraestrutura.
Fake à mão: o caminho idiomático
O fake mais útil é um repositório em memória. Ele é rápido, não exige biblioteca e se comporta como o de verdade o suficiente para testar a lógica:
package main
import (
"context"
"testing"
)
type MemoriaUsuario struct {
dados map[int]Usuario
}
func NewMemoriaUsuario() *MemoriaUsuario {
return &MemoriaUsuario{dados: make(map[int]Usuario)}
}
func (m *MemoriaUsuario) Salvar(_ context.Context, u Usuario) error {
m.dados[u.ID] = u
return nil
}
func (m *MemoriaUsuario) Buscar(_ context.Context, id int) (Usuario, error) {
u, ok := m.dados[id]
if !ok {
return Usuario{}, ErrNaoEncontrado
}
return u, nil
}
func TestServico_Criar_PersisteEGeraID(t *testing.T) {
repo := NewMemoriaUsuario()
svc := NovoServico(repo)
criado, err := svc.Criar(context.Background(), "Diego")
if err != nil {
t.Fatalf("erro inesperado: %v", err)
}
buscado, err := repo.Buscar(context.Background(), criado.ID)
if err != nil {
t.Fatalf("não encontrou o que salvou: %v", err)
}
if buscado.Nome != "Diego" {
t.Errorf("esperado Diego, veio %s", buscado.Nome)
}
}
Esse teste roda em microssegundos, não precisa de container e quebra nos momentos certos: quando a lógica de Criar muda de comportamento. Para dependências externas pesadas (PostgreSQL, Redis), guarde o Testcontainers para um conjunto menor de testes de integração — não para cada teste unitário.
httptest: handlers e clientes HTTP sem servidor
Para testar um handler HTTP, a biblioteca padrão oferece net/http/httptest. httptest.NewRequest monta a requisição e httptest.NewRecorder captura a resposta, sem subir servidor:
package main
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func criarUsuario(repo RepositorioUsuario) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
_ = repo
if r.Body == nil {
http.Error(w, "corpo vazio", http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusCreated)
}
}
func TestCriarUsuario_400_QuandoJSONInvalido(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/usuarios", strings.NewReader(`{inválido`))
rec := httptest.NewRecorder()
criarUsuario(NewMemoriaUsuario()).ServeHTTP(rec, req)
if rec.Code != http.StatusBadRequest && rec.Code != http.StatusCreated {
// handler ilustra o uso de httptest; ajuste a asserção à sua rota real
t.Fatalf("status inesperado: %d", rec.Code)
}
}
Para testar um cliente HTTP (o seu código que chama uma API externa), use httptest.NewServer para subir um servidor falso que devolve respostas controladas — assim você testa timeout, retry e parsing sem depender da internet. Lembre de configurar context com prazo nos testes que envolvem rede; o guia de context, timeout e cancelamento cobre o padrão.
testify/mock: asserções e expectativas leves
Quando você precisa garantir interações (a dependência foi chamada com quais argumentos, quantas vezes), testify/mock é a opção mais difundida no Brasil. Você embute mock.Mock na struct e chama Called:
package main
import (
"context"
"errors"
"testing"
"github.com/stretchr/testify/mock"
)
type MockRepositorioUsuario struct {
mock.Mock
}
func (m *MockRepositorioUsuario) Salvar(ctx context.Context, u Usuario) error {
args := m.Called(ctx, u)
return args.Error(0)
}
func TestServico_Criar_PropagaErroDoRepositorio(t *testing.T) {
repo := new(MockRepositorioUsuario)
repo.On("Salvar", mock.Anything, mock.Anything).
Return(errors.New("falha de conexão"))
svc := NovoServico(repo)
_, err := svc.Criar(context.Background(), "Diego")
if err == nil {
t.Fatal("esperado erro do repositório")
}
repo.AssertExpectations(t)
}
O ponto forte é a verbosidade baixa e o fato de o mock ser verificado em runtime, sem passo de geração de código. O ponto fraco também existe: como o nome do método é uma string ("Salvar"), o teste só falha quando roda, e um método renomeado não quebra a compilação.
gomock: mocks gerados e tipados
Em bases grandes, gomock (com mockgen) gera o mock a partir da interface e devolve chamadas tipadas. O teste falha na compilação quando a interface muda, o que escala melhor em times grandes:
package main
import (
"context"
"testing"
"go.uber.org/mock/gomock"
)
//go:generate go run go.uber.org/mock/mockgen -source=servico.go -destination=mock_usuario_test.go -package=main
func TestServico_Criar_ComGomock(t *testing.T) {
ctrl := gomock.NewController(t)
repo := NewMockRepositorioUsuario(ctrl)
repo.EXPECT().Salvar(gomock.Any(), Usuario{Nome: "Diego"}).Return(nil)
svc := NovoServico(repo)
if _, err := svc.Criar(context.Background(), "Diego"); err != nil {
t.Fatalf("erro inesperado: %v", err)
}
}
O custo é o passo de geração (go generate) e uma curva um pouco maior. Para projetos pequenos e médios, testify costuma pagar menos; em monorepos com dezenas de serviços, gomock evita mocks desatualizados.
Qual escolher
Decida nesta ordem:
- Lógica de domínio: fake à mão. É legível, rápido e não acopla o teste a detalhes de implementação.
- Handlers e clientes HTTP:
httptest, da biblioteca padrão. - Poucas interações a verificar:
testify/mock— leve e sem geração de código. - Muitas interfaces, time grande, refatorações frequentes:
gomock— paga o investimento em segurança de tipos.
Em qualquer caso, não mocke o que não é seu. Bancos, SDKs de nuvem e bibliotecas de terceiros mudam; abstraia-os atrás de uma interface própria e troque a implementação no teste. É exatamente o que o guia de API REST com Go recomenda para manter os handlers testáveis.
Armadilhas comuns
- Mockar tipos concretos: se o consumidor depende de um
*sql.DBdireto, não dá para trocar. Sempre depender de interface. - Excesso de
EXPECT: quando o teste lista cada chamada, qualquer refatoração inofensiva o quebra. Prefira fakes e reserve as expectativas estritas ao que realmente importa. - Testar o mock em vez do código: se o teste só verifica que você chamou o que você mandou chamar, ele não testa comportamento. Use fakes para verificar o resultado.
- Esquecer o
contexte o timeout: testes de cliente HTTP sem prazo ficam pendurados no CI. Sempre passe umcontextcomWithTimeout. - Ignorar caminhos de erro: table-driven tests com fakes cobrem felicidade e erro de graça — não teste só o caso que funciona.
Testes e carreira
Saber isolar dependências com test doubles é uma das habilidades mais cobradas em entrevistas técnicas de Go e aparece nas perguntas comuns de entrevista. Quem chega e mostra um serviço com interface pequena, fake em memória e tabela de testes passa uma imagem de senioridade que “só rodar no Postman” não passa. Para fechar o ciclo de qualidade, combine mocks com TDD e CI/CD e com fuzz testing para entradas inesperadas. A mesma ideia de “depender de interfaces para ficar testável” aparece em outras stacks: o guia de API REST em Python mostra como o padrão de injeção de dependência do FastAPI cumpre o mesmo papel de tornar o código testável sem acoplar a banco e serviços externos.