Testify: asserções e suítes de testes em Go
Como usar as asserções do Testify, a diferença entre assert e require e como organizar suítes de testes mais legíveis em Go.
Publicada em · 9 min de leitura
Embora o pacote nativo testing do Go seja intencionalmente minimalista e poderoso, a escrita de asserções em código puro exige blocos repetitivos de verificação manual (if got != want { t.Errorf(...) }). Em projetos de médio e grande porte, essa verbosidade pode poluir a legibilidade dos cenários e dificultar a inspeção de estruturas complexas como fatias desordenadas, datas e payloads JSON.
Para resolver esse atrito mantendo a compatibilidade estrita com a ferramenta padrão go test, a comunidade criou o Testify (github.com/stretchr/testify), o conjunto de ferramentas de testes mais utilizado no ecossistema Go corporativo.
Esta aula ensina a utilizar o Testify de ponta a ponta: a diferença vital entre os pacotes assert e require, as asserções idiomáticas mais produtivas para o dia a dia e a criação de suítes estruturadas de teste com ciclo de vida orientado a objetos através de suite.Suite.
O Mapa da Aula
| Parte | Seção | O que você vai dominar |
|---|---|---|
| 1. Fundamentos | 1 e 2 | O que é o Testify, por que ele não viola o idiomatismo do Go e como instalá-lo |
| 2. Assert vs Require | 3 e 4 | A diferença arquitetural entre t.Errorf e t.Fatalf, e a árvore de decisão para cada caso |
| 3. Catálogo de Asserções | 5 a 7 | Comparação de coleções sem ordem, validação temporal com WithinDuration e parsing JSON com JSONEq |
4. Suítes com suite.Suite | 8 a 10 | Struct embedding, hooks de ciclo de vida (SetupTest, TearDownSuite) e runner oficial |
| 5. Código de Produção | 11 e 12 | Implementação prática de um serviço completo, checklist de qualidade e armadilhas frequentes |
Pré-requisito recomendado: Table-Driven Tests e Paralelismo.
Parte 1: Fundamentos e Instalação
1. O que o Testify resolve
No Go padrão, comparar dois objetos ou verificar um erro exige este padrão clássico:
// Estilo nativo puro
if err != nil {
t.Fatalf("erro inesperado: %v", err)
}
if user.Nome != "Alice" {
t.Errorf("esperava nome Alice, recebeu %s", user.Nome)
}
if user.Idade != 30 {
t.Errorf("esperava idade 30, recebeu %d", user.Idade)
}
Com o Testify, a mesma verificação é expressa de forma fluida, com mensagens de erro ricas que mostram diffs coloridos de strings e estruturas:
// Estilo com Testify
require.NoError(t, err)
assert.Equal(t, "Alice", user.Nome)
assert.Equal(t, 30, user.Idade)
2. Instalação no módulo Go
Para adicionar o Testify ao seu projeto:
go get github.com/stretchr/testify
go mod tidy
O Testify é dividido em três módulos principais:
assert: Funções de asserção que continuam a execução se falharem (mapeia parat.Errorf).require: Funções de asserção idênticas aoassert, mas que abortam o teste imediatamente se falharem (mapeia parat.Fatalf).suite: Estrutura orientada a objetos para criar suítes completas com estado compartilhado e hooks de setup/teardown.
Parte 2: A Distinção Vital: assert vs require
A decisão de usar assert ou require é a escolha mais importante ao escrever testes com Testify:
3. Quando usar require (Falhas Críticas)
Use require sempre que a falha da asserção tornar qualquer linha de código seguinte inválida ou perigosa:
user, err := repo.BuscarUsuario(id)
// 🚨 OBRIGATÓRIO: require.NoError e require.NotNil
require.NoError(t, err)
require.NotNil(t, user)
// Se usássemos assert acima e err != nil ocorresse,
// a linha abaixo causaria PANIC por nil pointer dereference!
assert.Equal(t, "alice@email.com", user.Email)
Se require.NoError(t, err) falhar, o Testify invoca t.FailNow() por baixo dos panos, encerrando aquela goroutine de teste na hora e preservando os logs limpos.
4. Quando usar assert (Falhas Não-Críticas)
Use assert quando você estiver validando múltiplos campos independentes de um objeto ou resposta:
resp, err := client.ConsultarPerfil()
require.NoError(t, err)
require.NotNil(t, resp)
// Usar assert aqui é ideal: se o Nome falhar, ainda saberemos se o Email e Cargo bateram
assert.Equal(t, "Alice", resp.Nome)
assert.Equal(t, "alice@empresa.com", resp.Email)
assert.Equal(t, "Engenheira Sênior", resp.Cargo)
assert.True(t, resp.Ativo)
Se todos fossem require, uma falha no primeiro campo abortaria o teste antes de verificar os outros, obrigando o desenvolvedor a corrigir um campo de cada vez a cada execução.
Parte 3: Catálogo de Asserções Essenciais
O Testify disponibiliza dezenas de utilitários de asserção especializados. A tabela abaixo resume os mais relevantes para o desenvolvimento backend:
| Categoria | Asserção | Utilidade Prática |
|---|---|---|
| Erros | require.NoError(t, err) | Garante que a operação foi bem-sucedida |
| Erros | require.Error(t, err) | Garante que a operação gerou erro |
| Erros | assert.ErrorIs(t, err, ErrAlvo) | Valida se o erro encapsulado contém a causa sentinela |
| Valores | assert.Equal(t, esperado, recebido) | Compara igualdade de valores ou structs |
| Ponteiros | require.NotNil(t, ptr) | Evita pânico por ponteiro nulo |
| Coleções | assert.ElementsMatch(t, a, b) | Compara dois slices ignorando a ordem dos elementos |
| Coleções | assert.Contains(t, listaOuTexto, item) | Verifica se um slice, map ou string contém o elemento |
| Temporal | assert.WithinDuration(t, time.Now(), d, delta) | Valida se um timestamp gerado está dentro de uma margem aceitável |
| Payloads | assert.JSONEq(t, jsonEsperado, jsonRecebido) | Compara JSONs semanticamente, ignorando espaços e ordem de chaves |
| Segurança | assert.Panics(t, func) | Valida se uma operação dispara panic sob entrada malformada |
Exemplo 1: Comparando JSONs com assert.JSONEq
Testar respostas HTTP em JSON puro com assert.Equal costuma quebrar por causa de espaços, indentação ou ordem das chaves. O assert.JSONEq analisa o conteúdo semântico:
jsonEsperado := `{"id": 1, "status": "ATIVO"}`
jsonRecebido := `{"status": "ATIVO", "id": 1}`
// Passa com sucesso! Ordem de propriedades não importa no JSON
assert.JSONEq(t, jsonEsperado, jsonRecebido)
Exemplo 2: Validando Timestamps com assert.WithinDuration
Em bancos de dados e APIs, campos CreatedAt sofrem pequenas variações de milissegundos entre o momento da chamada e a asserção:
novoItem := CriarItem()
// Aceita se foi criado dentro de uma janela de 2 segundos a partir de agora
assert.WithinDuration(t, time.Now(), novoItem.CreatedAt, 2*time.Second)
Exemplo 3: Fatias sem ordem fixa com assert.ElementsMatch
Ideal para queries de banco ou coleções extraídas de maps cuja ordem de iteração não é garantida:
esperados := []string{"admin", "editor", "financeiro"}
recebidos := []string{"financeiro", "admin", "editor"}
assert.ElementsMatch(t, esperados, recebidos) // Passa!
Parte 4: Suítes Estruturadas com suite.Suite
Quando os testes necessitam de inicialização de conexões, subida de bancos em memória, transações isoladas ou limpeza de diretórios, o padrão de funções avulsas pode gerar código repetitivo de setup.
O pacote github.com/stretchr/testify/suite oferece uma abordagem estruturada baseada em structs com hooks de ciclo de vida:
8. Os Hooks de Ciclo de Vida
Para utilizar a suíte, basta declarar uma struct que incorpora suite.Suite:
type MinhaSuite struct {
suite.Suite
db *sql.DB
}
O Testify identifica e executa automaticamente métodos com assinaturas específicas de ciclo de vida:
SetupSuite(): Executado uma única vez antes do primeiro teste da suíte começar. Ideal para conexões caras (banco de dados, clientes HTTP, migrações de schema).SetupTest(): Executado antes de cada método de teste. Ideal para limpar tabelas, resetar mocks e reinicializar estado.TearDownTest(): Executado imediatamente após cada método de teste. Ideal para cancelar contextos ou reverter transações.TearDownSuite(): Executado uma única vez após todos os testes terminarem. Ideal para encerrar pools de conexão e derrubar recursos efêmeros.
9. Métodos de asserção na própria suíte (s.Assert() e s.Require())
Dentro dos métodos da suíte, você não precisa passar t manualmente para cada asserção. A struct embutida já fornece os atalhos s.Assert() e s.Require():
func (s *MinhaSuite) TestBuscarUsuario() {
u, err := s.db.Find(1)
s.Require().NoError(err)
s.Assert().Equal("Bob", u.Nome)
}
Parte 5: Código Prático Completo de Produção
Abaixo está uma implementação completa de um serviço de gerenciamento de usuários demonstrando o uso combinado de assert, require e suite.Suite.
O Código de Domínio (usuario.go):
package usuario
import (
"errors"
"strings"
"time"
)
var (
ErrDadosInvalidos = errors.New("dados do usuário são inválidos")
ErrEmailExistente = errors.New("e-mail já cadastrado no sistema")
)
type Usuario struct {
ID string
Nome string
Email string
Ativo bool
CreatedAt time.Time
}
type Repositorio interface {
Salvar(u *Usuario) error
BuscarPorEmail(email string) (*Usuario, bool)
}
type Servico struct {
repo Repositorio
}
func NovoServico(repo Repositorio) *Servico {
return &Servico{repo: repo}
}
func (s *Servico) Cadastrar(id, nome, email string) (*Usuario, error) {
if strings.TrimSpace(nome) == "" || !strings.Contains(email, "@") {
return nil, ErrDadosInvalidos
}
if _, existe := s.repo.BuscarPorEmail(email); existe {
return nil, ErrEmailExistente
}
u := &Usuario{
ID: id,
Nome: nome,
Email: email,
Ativo: true,
CreatedAt: time.Now(),
}
if err := s.repo.Salvar(u); err != nil {
return nil, err
}
return u, nil
}
A Suíte de Testes com Testify (usuario_test.go):
package usuario_test
import (
"testing"
"time"
"github.com/stretchr/testify/suite"
"meu-modulo/usuario"
)
// FakeRepositorio simula persistência em memória para testes isolados
type FakeRepositorio struct {
dados map[string]*usuario.Usuario
}
func NovoFakeRepositorio() *FakeRepositorio {
return &FakeRepositorio{dados: make(map[string]*usuario.Usuario)}
}
func (r *FakeRepositorio) Salvar(u *usuario.Usuario) error {
r.dados[u.Email] = u
return nil
}
func (r *FakeRepositorio) BuscarPorEmail(email string) (*usuario.Usuario, bool) {
u, ok := r.dados[email]
return u, ok
}
// Definição da Suíte de Testes
type UsuarioTestSuite struct {
suite.Suite
repo *FakeRepositorio
svc *usuario.Servico
}
// SetupSuite: executado uma única vez antes de todos os testes
func (s *UsuarioTestSuite) SetupSuite() {
// Ex: Inicialização de configurações globais
}
// SetupTest: executado antes de CADA teste (garante isolamento)
func (s *UsuarioTestSuite) SetupTest() {
s.repo = NovoFakeRepositorio()
s.svc = usuario.NovoServico(s.repo)
}
// Cenário 1: Cadastro realizado com sucesso
func (s *UsuarioTestSuite) TestCadastrar_Sucesso() {
u, err := s.svc.Cadastrar("u-10", "Alice Santos", "alice@empresa.com")
// require: falha crítica
s.Require().NoError(err)
s.Require().NotNil(u)
// assert: validação detalhada dos campos
s.Assert().Equal("u-10", u.ID)
s.Assert().Equal("Alice Santos", u.Nome)
s.Assert().Equal("alice@empresa.com", u.Email)
s.Assert().True(u.Ativo)
s.Assert().WithinDuration(time.Now(), u.CreatedAt, 2*time.Second)
}
// Cenário 2: Erro ao tentar cadastrar e-mail duplicado
func (s *UsuarioTestSuite) TestCadastrar_EmailDuplicado() {
_, err := s.svc.Cadastrar("u-1", "Carlos", "carlos@empresa.com")
s.Require().NoError(err)
// Segunda chamada com mesmo e-mail deve falhar
_, errDuplicado := s.svc.Cadastrar("u-2", "Outro Carlos", "carlos@empresa.com")
s.Require().Error(errDuplicado)
s.Assert().ErrorIs(errDuplicado, usuario.ErrEmailExistente)
}
// Cenário 3: Combinação de Suíte com Table-Driven Tests
func (s *UsuarioTestSuite) TestCadastrar_ValidacoesInvalidas() {
casos := []struct {
name string
id string
nome string
email string
}{
{name: "nome vazio", id: "1", nome: "", email: "user@email.com"},
{name: "apenas espaços no nome", id: "2", nome: " ", email: "user@email.com"},
{name: "email sem arroba", id: "3", nome: "User", email: "user.email.com"},
}
for _, tc := range casos {
s.Run(tc.name, func() {
u, err := s.svc.Cadastrar(tc.id, tc.nome, tc.email)
s.Require().Error(err)
s.Assert().Nil(u)
s.Assert().ErrorIs(err, usuario.ErrDadosInvalidos)
})
}
}
// TearDownSuite: executado ao final de todos os testes da suíte
func (s *UsuarioTestSuite) TearDownSuite() {
// Ex: Fechar conexões ou limpar diretórios temporários
}
// Ponto de entrada obrigatório para o `go test` disparar a suíte
func TestUsuarioTestSuite(t *testing.T) {
suite.Run(t, new(UsuarioTestSuite))
}
Parte 6: Checklist de Qualidade e Armadilhas Comuns
- Esquecer a função runner
TestXxx(t *testing.T): Métodos declarados emsuite.Suitenão são detectados diretamente pelo comandogo test. Você precisa definir uma função tradicional comsuite.Run(t, new(SuaSuite))para que o Go execute os testes da struct. - Usar
assertantes de acessar campos de um ponteiro retornado:// ❌ Erro perigoso: se err != nil, user provavelmente é nil assert.NoError(t, err) assert.Equal(t, "Alice", user.Nome) // Pode causar PANIC e derrubar a suíte! // Correto: interrompe antes do pânico require.NoError(t, err) require.NotNil(t, user) assert.Equal(t, "Alice", user.Nome) - Não poluir a ordem com
assert.Equal: A assinatura do Testify éassert.Equal(t, esperado, obtido). Embora inverter a ordem não quebre o teste, em caso de falha o relatório mostrará as mensagens trocadas: “Expected: [valor obtido], Actual: [valor esperado]”, confundindo a depuração. - Preferir
assert.ErrorIsem vez de verificar strings: Sempre que o seu erro puder ser comparado comerrors.Is, utilizeassert.ErrorIs(t, err, ErrEsperado). Só useassert.EqualErrorse a mensagem exata de formatação de string fizer parte do contrato estrito do sistema.
Exercício Prático Proposto
- Crie uma struct
CalculadoraFinanceiracom métodosCalcularJurosCompostos(capital, taxa, tempo float64) (float64, error). - Escreva uma suíte utilizando
suite.SuitecomSetupTest()reinicializando a calculadora. - Adicione asserções com
s.Require().NoError(err)e valide os valores calculados com precisão aproximada vias.Assert().InDelta(esperado, obtido, 0.01). - Adicione um método com testes orientados por tabela (
s.Run) validando entradas negativas ou inválidas coms.Assert().ErrorIs.