<h2>O Package Testing em Go: Fundamentos</h2>
<p>Go oferece uma abordagem minimalista e integrada para testes através do package <code>testing</code>. Diferente de frameworks pesados em outras linguagens, Go adota a filosofia Unix: fazer uma coisa e fazer bem. Os testes em Go são funções normais que seguem uma convenção simples: devem estar em arquivos terminados com <code>_test.go</code> e receber um parâmetro <code>*testing.T</code>.</p>
<p>A razão dessa simplicidade é profunda. Go foi desenhado para aplicações de servidor de grande escala, onde confiabilidade é crítica. Os criadores perceberam que testes complexos e difíceis de entender frequentemente não são mantidos. Portanto, a biblioteca padrão fornece exatamente o que você precisa, sem abstrações desnecessárias. Quando você cria um teste em Go, está escrevendo código que será lido dezenas de vezes e modificado constantemente — simplicidade não é um luxo, é um requisito.</p>
<pre><code class="language-go">package calculator
import "testing"
// Função que vamos testar
func Add(a, b int) int {
return a + b
}
// Teste básico usando testing.T
func TestAdd(t *testing.T) {
result := Add(2, 3)
expected := 5
if result != expected {
t.Errorf("Add(2, 3) = %d, want %d", result, expected)
}
}</code></pre>
<p>O parâmetro <code>*testing.T</code> oferece métodos para relatar falhas: <code>Error()</code>, <code>Errorf()</code>, <code>Fatal()</code> e <code>Fatalf()</code>. Use <code>Errorf()</code> quando o teste deve continuar (permitindo múltiplas asserções), e <code>Fatalf()</code> quando a falha indica um problema tão grave que continuar não faz sentido. Essa distinção é importante para escrever testes que forneçam informações úteis quando falham.</p>
<h2>Table-Driven Tests: Escalabilidade sem Repetição</h2>
<p>Table-driven tests resolvem um problema real: testar a mesma função com múltiplas entradas. A abordagem ingênua seria repetir o código de teste várias vezes — ineficiente e difícil de manter. A solução elegante de Go é usar uma slice de structs, onde cada struct contém os dados de entrada e a saída esperada.</p>
<p>Este padrão não é apenas uma convenção cultural em Go — é amplamente considerado a forma correta de escrever testes parametrizados. Grandes projetos como o Kubernetes e Docker usam table-driven tests extensivamente. A razão é simples: você consegue adicionar novos casos de teste sem escrever nenhuma função nova, apenas adicionando uma linha à tabela.</p>
<pre><code class="language-go">package calculator
import "testing"
func Divide(a, b int) (int, error) {
if b == 0 {
return 0, ErrDivisionByZero
}
return a / b, nil
}
func TestDivideTableDriven(t *testing.T) {
tests := []struct {
name string
a int
b int
want int
wantErr bool
}{
{
name: "positive numbers",
a: 10,
b: 2,
want: 5,
wantErr: false,
},
{
name: "division by zero",
a: 10,
b: 0,
want: 0,
wantErr: true,
},
{
name: "negative result",
a: -10,
b: 2,
want: -5,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Divide(tt.a, tt.b)
if (err != nil) != tt.wantErr {
t.Errorf("Divide(%d, %d) error = %v, wantErr %v",
tt.a, tt.b, err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("Divide(%d, %d) = %d, want %d",
tt.a, tt.b, got, tt.want)
}
})
}
}</code></pre>
<p>Observe o padrão: a slice contém uma struct anônima com campos que descrevem cada caso. O campo <code>name</code> é crucial — permite rodar casos específicos via <code>go test -run TestDivideTableDriven/positive</code>. Os nomes também aparecem no relatório de falhas, tornando imediatamente claro qual cenário quebrou. A iteração usa <code>t.Run()</code> para criar subtestes, que cobriremos em detalhes na próxima seção.</p>
<h3>Vantagens Práticas do Padrão</h3>
<p>O padrão table-driven torna fácil adicionar casos extremos sem modificar lógica. Se você descobrir um bug através de um relatório de usuário, pode reproduzir adicionando um caso à tabela. Isso cria um registro histórico de bugs que ajuda na revisão de código e documentação.</p>
<h2>Subtests: Isolamento e Composição</h2>
<p>Subtestes, introduzidos no Go 1.7, são testes aninhados criados via <code>t.Run()</code>. Cada subteste executa de forma isolada com seu próprio <code>*testing.T</code>, permitindo que falhas em um subteste não afetem outros. Isso é particularmente poderoso quando combinado com table-driven tests.</p>
<p>A semântica de execução é importante: se um teste falha, os subtestes subsequentes ainda executam. Isso fornece uma visão completa dos problemas. Você pode também interromper a execução condicionalmente usando <code>t.FailNow()</code> ou <code>t.Fatalf()</code>, o que encerra apenas aquele subteste.</p>
<pre><code class="language-go">package calculator
import "testing"
func TestCalculatorOperations(t *testing.T) {
// Subtest para validação de entrada
t.Run("input validation", func(t *testing.T) {
tests := []struct {
name string
a int
b int
valid bool
}{
{"valid inputs", 5, 3, true},
{"zero allowed", 0, 5, true},
{"negative allowed", -5, 3, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Validação simplificada
valid := tt.a >= -1000 && tt.b >= -1000
if valid != tt.valid {
t.Errorf("validation failed for (%d, %d)", tt.a, tt.b)
}
})
}
})
// Subtest para operações aritméticas
t.Run("arithmetic operations", func(t *testing.T) {
t.Run("addition", func(t *testing.T) {
if Add(2, 3) != 5 {
t.Error("addition failed")
}
})
t.Run("division", func(t *testing.T) {
result, err := Divide(10, 2)
if err != nil || result != 5 {
t.Error("division failed")
}
})
})
}</code></pre>
<p>Subtestes habilitam uma estrutura hierárquica nos seus testes. Você pode agrupar testes relacionados logicamente e ainda executar seletivamente via flags. Por exemplo, <code>go test -run TestCalculatorOperations/arithmetic</code> executa apenas o grupo de operações aritméticas. Isso é extremamente útil em projetos grandes onde querer iterar rapidamente em uma área específica.</p>
<h3>Usando Subtests com Cleanup</h3>
<p>Go 1.14 introduziu <code>t.Cleanup()</code>, permitindo registrar funções de limpeza que executam ao final de cada subteste. Isso elimina a necessidade de setUp/tearDown em estilos antigos:</p>
<pre><code class="language-go">func TestWithCleanup(t *testing.T) {
t.Run("test with setup", func(t *testing.T) {
// Setup
resource := acquireResource()
t.Cleanup(func() {
resource.Close()
})
// Seu teste usa resource aqui
if resource.Value() != expected {
t.Error("resource test failed")
}
})
}</code></pre>
<p>Isso garante limpeza mesmo se o teste falhar com <code>t.Fatal()</code>, uma garantia que setUp/tearDown tradicional não oferecia.</p>
<h2>Padrões Avançados e Boas Práticas</h2>
<h3>Testando Erros e Edge Cases</h3>
<p>Um erro comum é testar apenas o caminho feliz. Go incentiva tratamento explícito de erros, portanto seus testes devem fazer o mesmo. Estruture suas structs de teste para permitir testes robustos de casos de erro:</p>
<pre><code class="language-go">package validator
import (
"errors"
"testing"
)
var ErrInvalidEmail = errors.New("invalid email")
func ValidateEmail(email string) error {
if !contains(email, "@") {
return ErrInvalidEmail
}
return nil
}
func contains(s, substr string) bool {
// implementação
for i := 0; i < len(s)-len(substr)+1; i++ {
if s[i:i+len(substr)] == substr {
return true
}
}
return false
}
func TestValidateEmail(t *testing.T) {
tests := []struct {
name string
input string
wantErr error
}{
{name: "valid email", input: "user@example.com", wantErr: nil},
{name: "missing @", input: "userexample.com", wantErr: ErrInvalidEmail},
{name: "empty string", input: "", wantErr: ErrInvalidEmail},
{name: "only @", input: "@", wantErr: nil}, // Aceita para fins demo
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateEmail(tt.input)
if err != tt.wantErr {
t.Errorf("ValidateEmail(%q) error = %v, want %v",
tt.input, err, tt.wantErr)
}
})
}
}</code></pre>
<p>A chave aqui é nomear casos de erro de forma descritiva e testar tanto o valor de retorno quanto o erro. Não assuma que "sem erro" significa "funcionou" — verifique os valores reais.</p>
<h3>Organizando Testes em Arquivos</h3>
<p>A convenção é colocar testes no mesmo package do código que testam, em arquivos <code>_test.go</code>. Para packages grandes, você pode criar múltiplos arquivos de teste: <code>unit_test.go</code>, <code>integration_test.go</code>, <code>benchmark_test.go</code>. Use build tags para testes que devem ser opcionais:</p>
<pre><code class="language-go">// +build integration
package mypackage
import "testing"
func TestIntegration(t *testing.T) {
// Testes que requerem recursos externos
}</code></pre>
<p>Execute apenas testes de integração com <code>go test -tags=integration</code>. Essa separação mantém feedback rápido em testes unitários enquanto permite testes mais completos sob demanda.</p>
<h3>Exemplo Realista Completo</h3>
<p>Vamos ver um exemplo de um package mais complexo com testes bem estruturados:</p>
<pre><code class="language-go">package userstore
import "testing"
type User struct {
ID int
Name string
Email string
}
type UserStore struct {
users map[int]*User
nextID int
}
func NewUserStore() *UserStore {
return &UserStore{users: make(map[int]*User), nextID: 1}
}
func (s UserStore) Create(name, email string) (User, error) {
if name == "" || email == "" {
return nil, ErrInvalidInput
}
user := &User{ID: s.nextID, Name: name, Email: email}
s.users[s.nextID] = user
s.nextID++
return user, nil
}
func (s UserStore) GetByID(id int) (User, error) {
user, exists := s.users[id]
if !exists {
return nil, ErrNotFound
}
return user, nil
}
var (
ErrInvalidInput = errors.New("invalid input")
ErrNotFound = errors.New("user not found")
)
// Testes
func TestUserStore(t *testing.T) {
t.Run("create", func(t *testing.T) {
tests := []struct {
name string
userName string
email string
wantErr bool
}{
{name: "valid user", userName: "John", email: "john@example.com", wantErr: false},
{name: "empty name", userName: "", email: "john@example.com", wantErr: true},
{name: "empty email", userName: "John", email: "", wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
store := NewUserStore()
user, err := store.Create(tt.userName, tt.email)
if (err != nil) != tt.wantErr {
t.Errorf("Create() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr && user.Name != tt.userName {
t.Errorf("Create() user.Name = %s, want %s", user.Name, tt.userName)
}
})
}
})
t.Run("get by id", func(t *testing.T) {
store := NewUserStore()
created, _ := store.Create("Jane", "jane@example.com")
t.Run("existing user", func(t *testing.T) {
user, err := store.GetByID(created.ID)
if err != nil {
t.Fatalf("GetByID() error = %v, want nil", err)
}
if user.Name != "Jane" {
t.Errorf("GetByID() returned wrong user")
}
})
t.Run("non-existing user", func(t *testing.T) {
_, err := store.GetByID(9999)
if err != ErrNotFound {
t.Errorf("GetByID() error = %v, want ErrNotFound", err)
}
})
})
}</code></pre>
<p>Este exemplo mostra como estruturar testes reais: subtestes agrupam funcionalidades relacionadas, table-driven tests cobrem múltiplos cenários, e cada teste é independente (note como cada caso cria seu próprio <code>NewUserStore()</code>).</p>
<h2>Conclusão</h2>
<p>Dominando testes em Go, você aprendeu três conceitos complementares que trabalham juntos. O package <code>testing</code> fornece a base minimalista necessária para escrever testes confiáveis e mantíveis. Table-driven tests escalam seus testes sem repetição de código, permitindo cobrir casos extremos facilmente. Subtests organizam testes hierarquicamente, habilitando execução seletiva e isolamento claro de responsabilidades.</p>
<p>A verdadeira força surge quando você combina esses padrões em projetos reais. Um developer experiente em Go reconhece que testes simples e bem-organizados são mais valiosos que frameworks sofisticados. Quando seus testes são fáceis de ler, fácil é adicionar novos casos, e portanto mais código você testa. Isso reduz bugs em produção — o verdadeiro objetivo.</p>
<h2>Referências</h2>
<ul>
<li><a href="https://golang.org/pkg/testing/" target="_blank" rel="noopener noreferrer">Go Testing Package - Official Documentation</a></li>
<li><a href="https://golang.org/doc/effective_go#testing" target="_blank" rel="noopener noreferrer">Effective Go - Testing</a></li>
<li><a href="https://dave.cheney.net/2013/06/09/writing-table-driven-tests-in-go" target="_blank" rel="noopener noreferrer">Table Driven Tests - Dave Cheney</a></li>
<li><a href="https://golang.org/blog/subtests" target="_blank" rel="noopener noreferrer">Subtests and Sub-benchmarks - Go Blog</a></li>
</ul>
<p><!-- FIM --></p>