Pular para o conteúdo

Trilha de Go · parte 11 de 37

Testes unitários em Go com o pacote testing

Como escrever testes com o pacote testing do Go: convenções de arquivos, caixa-preta e caixa-branca, e a diferença entre Errorf e Fatalf.

Publicada em · 6 min de leitura

Tamanho da fonte:
Largura da coluna:

Diferente da maioria das linguagens de programação modernas — que dependem de frameworks externos de terceiros (como JUnit no Java, Jest/Mocha no Node.js ou PyTest no Python) —, o Go nasceu com uma suíte de testes completa, robusta e ultra-rápida embutida diretamente na ferramenta oficial do compilador (go test).

Os criadores do Go projetaram o sistema de testes para que testes fossem código Go comum e idiomático: sem anotações mágicas, sem reflexão oculta e sem DSLs complexas. Se você sabe programar em Go, você já sabe escrever testes em Go.

Esta aula ensina a mecânica fundamental do pacote nativo testing, as convenções de arquivos e pacotes, a distinção entre testes de caixa-preta e caixa-branca, e como medir a cobertura de código com relatórios visuais no navegador.


O Mapa da Aula

ParteSeçãoO que você vai dominar
1. Fundamentos1 e 2As regras sagradas de arquivos _test.go, assinaturas de funções e exclusão em produção
2. Tipos de Teste3Pacote interno (package foo) vs pacote externo (package foo_test - caixa-preta)
3. Mecânica do testing.T4 e 5t.Errorf, t.Fatalf, t.Logf e o papel crucial de t.Helper() em funções utilitárias
4. Comandos e Flags6Executando testes com -v, filtrando com -run e ativando o race detector com -race
5. Cobertura de Código7 e 8Gerando coverprofile e inspecionando linhas cobertas em HTML interativo

Parte 1: Fundamentos e Convenções do go test

1. As 3 Regras Sagradas

Para que o compilador do Go reconheça e execute seus testes unitários, você só precisa seguir três convenções canônicas:

anatomia teste unitario go
figura 1 · anatomia teste unitario go
  1. Nome do Arquivo: O arquivo de teste deve obrigatoriamente terminar com o sufixo _test.go (por exemplo: se o seu código está em calculadora.go, o teste deve ficar em calculadora_test.go no mesmo diretório).
  2. Nome da Função: A função de teste deve iniciar com a palavra Test seguida por uma letra maiúscula (ex: TestSoma, TestValidarEmail). Funções como testSoma (com ‘t’ minúsculo) são ignoradas pelo runner.
  3. Assinatura da Função: A função deve receber exatamente um parâmetro do tipo ponteiro para testing.T:
    func TestNomeDoCenario(t *testing.T) {
        // lógica de teste
    }

Isolamento no Binário de Produção

Quando você executa go build para compilar sua aplicação para produção, o compilador do Go ignora completamente todos os arquivos terminados em _test.go. Eles nunca entram no executável final de produção, garantindo binários leves e livres de código de teste.


Parte 2: Pacote Interno vs Pacote Externo (Caixa-Preta)

No topo do seu arquivo _test.go, você tem duas opções de declaração de pacote:

1. Testes de Caixa-Branca (package calculadora)

Se você declarar o mesmo nome do pacote sob teste, seu teste terá acesso a funções e structs privadas (não exportadas) iniciadas com letra minúscula. É útil quando você precisa testar um algoritmo matemático interno complexo sem expô-lo para o público.

2. Testes de Caixa-Preta (package calculadora_test) — Recomendado

Ao adicionar o sufixo _test ao nome do pacote, o Go trata o teste como se ele fosse um consumidor externo do seu módulo. Você é obrigado a importar o pacote (import "meu-modulo/calculadora") e só consegue acessar métodos públicos (iniciados com letra maiúscula).


Parte 3: A Mecânica das Asserções Nativas

Por que Go não tem assert.Equal nativo na biblioteca padrão? Os criadores do Go explicaram que asserções com if comum forçam mensagens de erro mais ricas e descritivas:

func TestDividir(t *testing.T) {
    got, err := Dividir(10, 2)
    if err != nil {
        t.Fatalf("Dividir(10, 2) retornou erro inesperado: %v", err)
    }
    if got != 5 {
        t.Errorf("Dividir(10, 2) = %d; esperado = 5", got)
    }
}

4. t.Errorf vs t.Fatalf

  • t.Errorf (Falha Não-Crítica): Registra que o teste falhou e imprime a mensagem formatada no terminal, mas continua a execução das próximas linhas do teste. Use para validar valores divergentes onde você quer ver todos os campos com erro de uma vez.
  • t.Fatalf (Falha Fatal): Registra a falha e aborta a execução do teste imediatamente chamando t.FailNow(). Use para pré-condições onde continuar causaria um panic (ex: se err != nil, você não deve tentar ler o objeto de retorno).

5. O papel de t.Helper() em funções auxiliares

Quando criamos funções utilitárias para evitar duplicação em testes (por exemplo, uma função que cria um usuário de teste), se uma asserção falhar dentro dela, o Go reportará a linha interna da função auxiliar, dificultando saber qual teste específico falhou.

Ao declarar t.Helper() na primeira linha da sua função auxiliar, o runtime do Go remove esse frame do stack trace e reporta a linha exata do teste chamador:

func assertStringIgual(t *testing.T, esperado, obtido string) {
    t.Helper() // Avisa o compilador: reporte o erro na linha de quem me chamou!
    if obtido != esperado {
        t.Errorf("esperava %q, recebeu %q", esperado, obtido)
    }
}

Parte 4: Comandos Essenciais do go test

  • Executar todos os testes do projeto recursivamente:
    go test ./...
  • Modo Verboso (-v): Exibe o nome de cada função executada, o status (RUN, PASS, FAIL) e as mensagens emitidas por t.Logf:
    go test -v ./...
  • Executar apenas testes específicos (-run): Aceita uma expressão regular para filtrar pelo nome da função:
    # Executa apenas testes que tenham "Email" no nome
    go test -v -run Email ./...
  • Verificação de Condições de Corrida (-race): Compila o código com instrumentação do ThreadSanitizer e detecta qualquer acesso concorrente inseguro:
    go test -race ./...

Parte 5: Medindo Cobertura de Código

Saber quais linhas do seu código foram exercitadas pelos testes é fundamental para encontrar caminhos esquecidos:

1. Medição no terminal com -cover:

$ go test -cover ./...
ok      meu-modulo/calculadora      0.012s      coverage: 87.5% of statements

2. Gerando relatório detalhado de cobertura:

# Grava as estatísticas linha a linha no arquivo coverage.out
go test -coverprofile=coverage.out ./...

3. Visualização interativa no navegador:

# Abre o navegador exibindo o código-fonte em verde (coberto) e vermelho (não coberto)
go tool cover -html=coverage.out

Parte 6: Código Prático Completo

Código de Negócio (validador.go):

package validador

import (
	"errors"
	"strings"
)

var (
	ErrEmailVazio    = errors.New("o e-mail não pode ser vazio")
	ErrDominioInvalido = errors.New("o domínio do e-mail é inválido")
)

// ValidarEmailCorporativo verifica se o e-mail possui formato e domínio válidos.
func ValidarEmailCorporativo(email, dominioEsperado string) (bool, error) {
	emailLimpo := strings.TrimSpace(email)
	if emailLimpo == "" {
		return false, ErrEmailVazio
	}

	partes := strings.Split(emailLimpo, "@")
	if len(partes) != 2 || partes[0] == "" || partes[1] == "" {
		return false, errors.New("formato de e-mail malformado")
	}

	if partes[1] != dominioEsperado {
		return false, ErrDominioInvalido
	}

	return true, nil
}

Código de Teste (validador_test.go):

package validador_test

import (
	"errors"
	"testing"

	"meu-modulo/validador"
)

// Função auxiliar com t.Helper()
func assertErroContem(t *testing.T, err error, esperado error) {
	t.Helper()
	if !errors.Is(err, esperado) {
		t.Fatalf("erro recebido = %v; esperava conter = %v", err, esperado)
	}
}

func TestValidarEmailCorporativo_Sucesso(t *testing.T) {
	valido, err := validador.ValidarEmailCorporativo("dev@empresa.com", "empresa.com")
	if err != nil {
		t.Fatalf("esperava sucesso, recebeu erro: %v", err)
	}
	if !valido {
		t.Errorf("esperava email válido, recebeu falso")
	}
}

func TestValidarEmailCorporativo_EmailVazio(t *testing.T) {
	valido, err := validador.ValidarEmailCorporativo("   ", "empresa.com")
	if err == nil {
		t.Fatalf("esperava erro para email vazio, mas retornou nil")
	}
	if valido {
		t.Errorf("esperava que valido fosse falso")
	}
	assertErroContem(t, err, validador.ErrEmailVazio)
}

func TestValidarEmailCorporativo_DominioIncorreto(t *testing.T) {
	valido, err := validador.ValidarEmailCorporativo("dev@gmail.com", "empresa.com")
	if err == nil {
		t.Fatalf("esperava erro para domínio divergente, retornou nil")
	}
	if valido {
		t.Errorf("esperava que valido fosse falso")
	}
	assertErroContem(t, err, validador.ErrDominioInvalido)
}

↑ 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
?