Testes são uma parte essencial do desenvolvimento de software em Go. A linguagem foi projetada com testes em mente, oferecendo um pacote padrão chamado testing que fornece todas as ferramentas necessárias para escrever testes unitários, benchmarks e exemplos. Nesta aula, você aprenderá os fundamentos de testes em Go, desde a criação de funções de teste até técnicas avançadas como testes orientados por tabela.

Escrever testes em Go é simples e direto. O compilador e as ferramentas da linguagem integram-se perfeitamente ao fluxo de trabalho, permitindo que você rode testes com um único comando: go test. Vamos explorar cada aspecto em detalhes, com exemplos práticos que você pode adaptar aos seus próprios projetos.

Pacote testing

O pacote testing é a base para a escrita de testes em Go. Ele fornece o tipo T, que é usado em funções de teste para relatar falhas e controlar o fluxo do teste. Além disso, o pacote oferece suporte a benchmarks (B) e exemplos (Example), mas o foco principal aqui são os testes unitários.

Para usar o pacote, você precisa importá-lo no seu arquivo de teste. Arquivos de teste devem ter o sufixo _test.go e geralmente ficam no mesmo pacote do código que estão testando. Por exemplo, se você tem um arquivo math.go com funções matemáticas, o teste correspondente estará em math_test.go.

O tipo T possui métodos importantes como Error, Fatal, Log e Fail. O método Error registra uma falha e continua a execução do teste, enquanto Fatal registra uma falha e interrompe imediatamente o teste. Log registra uma mensagem de log, útil para depuração.

package math

import "testing"

func TestAdd(t *testing.T) {
    result := Add(2, 3)
    if result != 5 {
        t.Errorf("Add(2, 3) = %d; want 5", result)
    }
}

Neste exemplo, a função TestAdd testa uma função Add. Se o resultado não for o esperado, o teste falha com uma mensagem clara.

func TestXxx

A convenção para funções de teste em Go é nomeá-las começando com Test, seguido por uma descrição do que está sendo testado (geralmente o nome da função). A assinatura deve ser func TestXxx(t *testing.T), onde Xxx pode ser qualquer nome que comece com letra maiúscula (para ser exportado) ou minúscula. O parâmetro t *testing.T é obrigatório.

Essas funções são reconhecidas automaticamente pelo comando go test. Não é necessário registrá-las explicitamente; basta definir a função no arquivo _test.go. O go test varre o pacote e executa todas as funções que seguem essa convenção.

Um ponto importante: o nome da função deve ser único dentro do pacote de teste. Se você tiver dois testes com o mesmo nome, o compilador reclamará. Além disso, é comum usar nomes descritivos que indiquem o comportamento esperado, como TestDivisionByZero ou TestParseValidInput.

package main

import "testing"

func TestIsEven(t *testing.T) {
    if !IsEven(4) {
        t.Error("expected 4 to be even")
    }
}

func TestIsOdd(t *testing.T) {
    if !IsOdd(3) {
        t.Error("expected 3 to be odd")
    }
}

No exemplo acima, temos duas funções de teste, cada uma verificando um aspecto diferente da lógica. Isso mantém os testes organizados e facilita a localização de falhas.

go test

O comando go test é a ferramenta central para executar testes em Go. Ele compila o pacote atual (ou o pacote especificado) e executa todas as funções de teste encontradas. O comando oferece várias flags para controlar o comportamento, como -v (verbose) para exibir detalhes de cada teste, -run para filtrar testes por nome, e -cover para medir a cobertura de código.

Para rodar testes em um pacote específico, você pode usar go test ./pkg/... ou go test ./... para todos os pacotes. A saída padrão mostra um resumo: se todos os testes passarem, exibe ok; caso contrário, mostra falhas detalhadas.

Exemplo de execução com verbosidade:

$ go test -v
=== RUN   TestAdd
--- PASS: TestAdd (0.00s)
=== RUN   TestSubtract
--- PASS: TestSubtract (0.00s)
PASS
ok      exemplo/matematica    0.002s

O comando go test também suporta a execução de benchmarks e exemplos, mas esses são tópicos avançados. Para a maioria dos casos, você usará apenas as funções de teste.

Uma dica: use go test -run TestNome para executar apenas um teste específico, útil durante o desenvolvimento.

Table-driven tests

Table-driven tests (testes orientados por tabela) são uma técnica poderosa em Go para testar múltiplos casos de entrada de forma concisa e legível. Em vez de escrever um bloco if para cada caso, você define uma tabela com entradas e saídas esperadas, e itera sobre ela usando um loop.

Essa abordagem reduz a duplicação de código, facilita a adição de novos casos e torna os testes mais legíveis. É uma prática recomendada pela comunidade Go e amplamente utilizada em projetos reais.

A estrutura básica é uma slice de structs, onde cada struct representa um caso de teste com campos como name, input e expected. Dentro do loop, você executa a função sendo testada e compara o resultado com o esperado, usando t.Run para executar subtestes com nomes descritivos.

package math

import "testing"

func TestAdd(t *testing.T) {
    tests := []struct {
        name     string
        a, b     int
        expected int
    }{
        {"positive numbers", 2, 3, 5},
        {"negative numbers", -2, -3, -5},
        {"zero values", 0, 0, 0},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result := Add(tt.a, tt.b)
            if result != tt.expected {
                t.Errorf("Add(%d, %d) = %d; want %d", tt.a, tt.b, result, tt.expected)
            }
        })
    }
}

Neste exemplo, a tabela tem três casos. O loop itera sobre eles e executa um subteste para cada um. Se um caso falhar, a mensagem incluirá o nome do subteste, facilitando a identificação do problema.

Table-driven tests são especialmente úteis quando você tem muitas variações de entrada e saída. Eles também incentivam a pensar em casos de borda, como valores negativos, zero, valores extremos, etc.

Boas práticas e observações finais

Ao escrever testes em Go, lembre-se de:

  • Testar casos de borda e entradas inválidas, além dos casos felizes.
  • Usar nomes descritivos para testes e subtestes.
  • Manter os testes independentes e isolados; cada teste deve ser executável sozinho.
  • Evitar depender de ordem de execução ou de estado global.
  • Usar t.Helper() em funções auxiliares para melhorar mensagens de erro.

Testes são parte integrante do desenvolvimento em Go. Com o pacote testing, você pode escrever testes robustos e manter a qualidade do seu código. Pratique os conceitos desta aula e explore mais recursos, como benchmarks e exemplos.

Referências

Exercícios

  1. Escreva uma função Divide que receba dois inteiros e retorne o quociente e um erro se o divisor for zero. Crie um teste TestDivide que verifique o resultado para divisões normais e o erro para divisão por zero.

✓ Resposta:
package main

import "errors"

func Divide(a, b int) (int, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}
package main

import "testing"

func TestDivide(t *testing.T) {
    // Caso normal
    result, err := Divide(10, 2)
    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }
    if result != 5 {
        t.Errorf("Divide(10, 2) = %d; want 5", result)
    }

    // Divisão por zero
    _, err = Divide(10, 0)
    if err == nil {
        t.Error("expected error for division by zero")
    }
}
  • Crie um teste table-driven para a função IsPalindrome que verifica se uma string é um palíndromo (ignorando maiúsculas/minúsculas e espaços). Inclua casos como "ana", "arara", "hello", "A man a plan a canal Panama".
  • ✓ Resposta:
    package main
    
    import "testing"
    
    func TestIsPalindrome(t *testing.T) {
        tests := []struct {
            name string
            input string
            want bool
        }{
            {"simple palindrome", "ana", true},
            {"another palindrome", "arara", true},
            {"not palindrome", "hello", false},
            {"palindrome with spaces", "A man a plan a canal Panama", true},
        }
    
        for _, tt := range tests {
            t.Run(tt.name, func(t *testing.T) {
                got := IsPalindrome(tt.input)
                if got != tt.want {
                    t.Errorf("IsPalindrome(%q) = %v; want %v", tt.input, got, tt.want)
                }
            })
        }
    }
  • Execute os testes do exercício anterior usando go test -v e explique a saída. O que acontece se um teste falhar?
  • ✓ Resposta:

    Ao executar go test -v, cada teste é listado com seu nome e status (PASS ou FAIL). Se um teste falhar, o Go imprime a mensagem de erro e o teste é marcado como FAIL. No final, o comando retorna um código de saída diferente de zero, indicando falha. Por exemplo, se a função IsPalindrome não estiver implementada corretamente, você verá algo como:

    === RUN   TestIsPalindrome
    === RUN   TestIsPalindrome/simple_palindrome
        main_test.go:20: IsPalindrome("ana") = false; want true
    --- FAIL: TestIsPalindrome (0.00s)
        --- FAIL: TestIsPalindrome/simple_palindrome (0.00s)
    FAIL

    Isso ajuda a identificar rapidamente qual caso falhou.

  • Modifique o teste table-driven do exercício 2 para usar t.Run com subtestes e inclua um caso de teste que verifique a função com uma string vazia.
  • ✓ Resposta:
    package main
    
    import "testing"
    
    func TestIsPalindrome(t *testing.T) {
        tests := []struct {
            name string
            input string
            want bool
        }{
            {"simple palindrome", "ana", true},
            {"another palindrome", "arara", true},
            {"not palindrome", "hello", false},
            {"palindrome with spaces", "A man a plan a canal Panama", true},
            {"empty string", "", true}, // string vazia é considerada palíndromo?
        }
    
        for _, tt := range tests {
            t.Run(tt.name, func(t *testing.T) {
                got := IsPalindrome(tt.input)
                if got != tt.want {
                    t.Errorf("IsPalindrome(%q) = %v; want %v", tt.input, got, tt.want)
                }
            })
        }
    }
  • Escreva um teste para uma função Fatorial que calcula o fatorial de um número inteiro não negativo. Use table-driven tests para cobrir os casos 0, 1, 5 e um caso que espera erro para números negativos.
  • ✓ Resposta:
    package main
    
    import "errors"
    
    func Fatorial(n int) (int, error) {
        if n < 0 {
            return 0, errors.New("negative number")
        }
        result := 1
        for i := 2; i <= n; i++ {
            result *= i
        }
        return result, nil
    }
    package main
    
    import "testing"
    
    func TestFatorial(t *testing.T) {
        tests := []struct {
            name    string
            input   int
            want    int
            wantErr bool
        }{
            {"zero", 0, 1, false},
            {"one", 1, 1, false},
            {"five", 5, 120, false},
            {"negative", -1, 0, true},
        }
    
        for _, tt := range tests {
            t.Run(tt.name, func(t *testing.T) {
                got, err := Fatorial(tt.input)
                if (err != nil) != tt.wantErr {
                    t.Errorf("Fatorial(%d) error = %v, wantErr %v", tt.input, err, tt.wantErr)
                    return
                }
                if got != tt.want {
                    t.Errorf("Fatorial(%d) = %d; want %d", tt.input, got, tt.want)
                }
            })
        }
    }