Pular para o conteúdo

Trilha de Go · parte 13 de 37

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

Tamanho da fonte:
Largura da coluna:

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

ParteSeçãoO que você vai dominar
1. Fundamentos1 e 2O que é o Testify, por que ele não viola o idiomatismo do Go e como instalá-lo
2. Assert vs Require3 e 4A diferença arquitetural entre t.Errorf e t.Fatalf, e a árvore de decisão para cada caso
3. Catálogo de Asserções5 a 7Comparação de coleções sem ordem, validação temporal com WithinDuration e parsing JSON com JSONEq
4. Suítes com suite.Suite8 a 10Struct embedding, hooks de ciclo de vida (SetupTest, TearDownSuite) e runner oficial
5. Código de Produção11 e 12Implementaçã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:

  1. assert: Funções de asserção que continuam a execução se falharem (mapeia para t.Errorf).
  2. require: Funções de asserção idênticas ao assert, mas que abortam o teste imediatamente se falharem (mapeia para t.Fatalf).
  3. 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:

assert vs require
figura 1 · assert vs require

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:

CategoriaAsserçãoUtilidade Prática
Errosrequire.NoError(t, err)Garante que a operação foi bem-sucedida
Errosrequire.Error(t, err)Garante que a operação gerou erro
Errosassert.ErrorIs(t, err, ErrAlvo)Valida se o erro encapsulado contém a causa sentinela
Valoresassert.Equal(t, esperado, recebido)Compara igualdade de valores ou structs
Ponteirosrequire.NotNil(t, ptr)Evita pânico por ponteiro nulo
Coleçõesassert.ElementsMatch(t, a, b)Compara dois slices ignorando a ordem dos elementos
Coleçõesassert.Contains(t, listaOuTexto, item)Verifica se um slice, map ou string contém o elemento
Temporalassert.WithinDuration(t, time.Now(), d, delta)Valida se um timestamp gerado está dentro de uma margem aceitável
Payloadsassert.JSONEq(t, jsonEsperado, jsonRecebido)Compara JSONs semanticamente, ignorando espaços e ordem de chaves
Segurançaassert.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:

ciclo vida suite testify
figura 2 · ciclo vida suite testify

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

  1. Esquecer a função runner TestXxx(t *testing.T): Métodos declarados em suite.Suite não são detectados diretamente pelo comando go test. Você precisa definir uma função tradicional com suite.Run(t, new(SuaSuite)) para que o Go execute os testes da struct.
  2. Usar assert antes 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)
  3. 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.
  4. Preferir assert.ErrorIs em vez de verificar strings: Sempre que o seu erro puder ser comparado com errors.Is, utilize assert.ErrorIs(t, err, ErrEsperado). Só use assert.EqualError se a mensagem exata de formatação de string fizer parte do contrato estrito do sistema.

Exercício Prático Proposto

  1. Crie uma struct CalculadoraFinanceira com métodos CalcularJurosCompostos(capital, taxa, tempo float64) (float64, error).
  2. Escreva uma suíte utilizando suite.Suite com SetupTest() reinicializando a calculadora.
  3. Adicione asserções com s.Require().NoError(err) e valide os valores calculados com precisão aproximada via s.Assert().InDelta(esperado, obtido, 0.01).
  4. Adicione um método com testes orientados por tabela (s.Run) validando entradas negativas ou inválidas com s.Assert().ErrorIs.

↑ Voltar ao topo

Atalhos de teclado

Abrir busca
/
Alternar tema claro/escuro
t
Aula anterior na série
←
Próxima aula na série
→
Lista de atalhos
?