← Voltar para o blog

Mocks em Go: Testify, gomock, Fakes e httptest na Prática

Mocks em Go na prática: test doubles, httptest, fakes à mão, testify/mock e gomock. Aprenda quando usar cada um e testar serviços sem depender de banco ou API externa.

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 doubleO que fazQuando usar
StubDevolve respostas fixas, ignora como foi chamadoIsolar uma entrada simples
FakeImplementação real, porém simplificada (ex.: repositório em memória)Testar lógica de domínio de ponta a ponta
SpyRegistra chamadas para você inspecionar depoisVerificar que algo foi chamado, sem bloquear
MockVem com expectativas; falha se o comportamento não ocorrerGarantir 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:

  1. Lógica de domínio: fake à mão. É legível, rápido e não acopla o teste a detalhes de implementação.
  2. Handlers e clientes HTTP: httptest, da biblioteca padrão.
  3. Poucas interações a verificar: testify/mock — leve e sem geração de código.
  4. 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.DB direto, 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 context e o timeout: testes de cliente HTTP sem prazo ficam pendurados no CI. Sempre passe um context com WithTimeout.
  • 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.