Testes são uma parte fundamental do desenvolvimento de software, pois garantem que o código funcione como esperado e facilitam mudanças futuras. Em Go, a linguagem foi projetada com testes em mente, oferecendo ferramentas nativas e uma abordagem simples e eficiente. Nesta aula, você aprenderá a escrever testes unitários usando o pacote testing, a convenção func TestXxx, o comando go test e a técnica de table-driven tests, que é amplamente adotada na comunidade Go.

Vamos começar com os fundamentos e, aos poucos, avançar para padrões mais sofisticados, sempre com exemplos práticos e explicações detalhadas. Ao final, você terá uma base sólida para testar seus próprios programas em Go.

Pacote testing

O pacote testing é parte da biblioteca padrão de Go e fornece suporte para testes automatizados. Ele define tipos e funções que permitem escrever testes unitários, benchmarks e exemplos. O tipo mais importante é *testing.T, usado para controlar testes de unidade. Com ele, você pode relatar falhas, pular testes, registrar logs e muito mais.

Para usar o pacote, basta importá-lo em um arquivo de teste. Convencionalmente, arquivos de teste terminam com _test.go, e o Go os reconhece automaticamente quando você executa go test. O pacote testing também oferece funções como t.Errorf, t.Fatalf, t.Logf, t.Skip, entre outras.

Exemplo básico de um arquivo de teste:

package main

import (
    "testing"
)

func TestSoma(t *testing.T) {
    resultado := soma(2, 3)
    if resultado != 5 {
        t.Errorf("soma(2, 3) = %d; esperado 5", resultado)
    }
}

Note que a função de teste recebe um parâmetro *testing.T, que é usado para relatar falhas. O método Errorf registra uma falha, mas permite que o teste continue; já Fatalf interrompe o teste imediatamente.

func TestXxx

Em Go, funções de teste seguem a convenção func TestXxx(t *testing.T), onde Xxx é um nome descritivo que começa com letra maiúscula. O nome deve indicar o que está sendo testado. Por exemplo, TestSoma, TestDivide, TestParseJSON. Essa convenção é reconhecida pelo comando go test, que executa todas as funções com esse padrão.

Além de testes de unidade, você pode definir funções com nomes BenchmarkXxx para benchmarks e ExampleXxx para exemplos (que também funcionam como testes). O prefixo Test é reservado para testes, e a função deve ter exatamente um parâmetro do tipo *testing.T.

Exemplo de múltiplos testes:

package main

import "testing"

func TestSoma(t *testing.T) {
    if soma(1, 2) != 3 {
        t.Error("soma(1,2) != 3")
    }
}

func TestSubtracao(t *testing.T) {
    if subtracao(5, 3) != 2 {
        t.Error("subtracao(5,3) != 2")
    }
}

É importante que as funções de teste sejam independentes e não dependam de ordem de execução. Cada teste deve verificar uma unidade de código isolada.

go test

O comando go test é a ferramenta principal para executar testes em Go. Ele compila o pacote atual e executa todas as funções de teste encontradas nos arquivos _test.go. Por padrão, ele roda os testes do diretório atual, mas você pode especificar pacotes ou usar flags para controlar o comportamento.

Algumas flags úteis:

  • -v: exibe detalhes de cada teste executado (verbose).
  • -run: filtra testes por padrão de nome, por exemplo, -run TestSoma.
  • -count: repete testes, útil para detectar flakiness.
  • -cover: mostra a cobertura de código (porcentagem de linhas executadas pelos testes).

Exemplo de execução:

go test -v ./...

Esse comando executa os testes de todos os pacotes do módulo, exibindo detalhes. O go test também gera um cache de resultados, acelerando execuções subsequentes.

Além de testes, o go test pode executar benchmarks e exemplos. Benchmarks são funções com prefixo Benchmark e são executados com a flag -bench.

Table-driven tests

Table-driven tests são uma técnica onde você define uma tabela (slice de structs) com casos de teste, cada um contendo entradas e saídas esperadas. Em seguida, você itera sobre a tabela, executando o mesmo código de teste para cada caso. Isso reduz duplicação e torna os testes mais fáceis de expandir.

O padrão é simples: defina uma struct para o caso de teste, crie uma slice com todos os casos e use um loop com t.Run para subtests. t.Run permite executar subtestes nomeados, o que melhora a saída e permite executar um subteste específico.

Exemplo de table-driven test para a função soma:

package main

import "testing"

func TestSoma(t *testing.T) {
    testes := []struct {
        nome     string
        a, b     int
        esperado int
    }{
        {"positivos", 1, 2, 3},
        {"negativos", -1, -2, -3},
        {"zero", 0, 0, 0},
    }

    for _, tt := range testes {
        t.Run(tt.nome, func(t *testing.T) {
            resultado := soma(tt.a, tt.b)
            if resultado != tt.esperado {
                t.Errorf("soma(%d, %d) = %d; esperado %d", tt.a, tt.b, resultado, tt.esperado)
            }
        })
    }
}

Essa abordagem é amplamente utilizada em projetos Go porque facilita a adição de novos casos e torna o teste mais legível. Além disso, com -run TestSoma/positivos você pode executar um caso específico.

Boas práticas e observações

Ao escrever testes em Go, lembre-se de:

  • Manter os testes independentes e determinísticos.
  • Usar nomes descritivos para funções de teste e subtests.
  • Testar casos de borda, como entradas vazias, zero, valores negativos, etc.
  • Usar t.Cleanup para liberar recursos, se necessário.
  • Executar go test -cover para verificar a cobertura e identificar áreas não testadas.

Referências

Exercícios

  1. Crie uma função Multiplica que receba dois inteiros e retorne o produto. Escreva um teste TestMultiplica que verifique o resultado para alguns casos simples.

    ✓ Resposta:
    package main
    
    import "testing"
    
    func Multiplica(a, b int) int {
        return a * b
    }
    
    func TestMultiplica(t *testing.T) {
        casos := []struct{ a, b, esperado int }{
            {2, 3, 6},
            {0, 5, 0},
            {-1, 4, -4},
        }
        for _, c := range casos {
            got := Multiplica(c.a, c.b)
            if got != c.esperado {
                t.Errorf("Multiplica(%d, %d) = %d; esperado %d", c.a, c.b, got, c.esperado)
            }
        }
    }
  2. Escreva uma função Divide que retorne o quociente e o resto da divisão de dois inteiros. Crie um teste table-driven para verificar vários casos, incluindo divisão por zero (trate o erro).

    ✓ Resposta:
    package main
    
    import "testing"
    
    func Divide(a, b int) (int, int, error) {
        if b == 0 {
            return 0, 0, errors.New("divisão por zero")
        }
        return a / b, a % b, nil
    }
    
    func TestDivide(t *testing.T) {
        casos := []struct {
            nome    string
            a, b    int
            quoc, resto int
            erro    bool
        }{
            {"divisão normal", 10, 3, 3, 1, false},
            {"divisão exata", 8, 2, 4, 0, false},
            {"por zero", 5, 0, 0, 0, true},
        }
        for _, c := range casos {
            t.Run(c.nome, func(t *testing.T) {
                q, r, err := Divide(c.a, c.b)
                if (err != nil) != c.erro {
                    t.Errorf("erro esperado? %v; obtido %v", c.erro, err)
                }
                if q != c.quoc || r != c.resto {
                    t.Errorf("Divide(%d, %d) = (%d, %d); esperado (%d, %d)", c.a, c.b, q, r, c.quoc, c.resto)
                }
            })
        }
    }
  3. Usando t.Run, escreva um teste para uma função Palindromo(s string) bool que verifica se a string é um palíndromo. Inclua casos com letras maiúsculas e espaços (ignore espaços e case).

    ✓ Resposta:
    package main
    
    import (
        "strings"
        "testing"
    )
    
    func Palindromo(s string) bool {
        s = strings.ToLower(strings.ReplaceAll(s, " ", ""))
        for i := 0; i < len(s)/2; i++ {
            if s[i] != s[len(s)-1-i] {
                return false
            }
        }
        return true
    }
    
    func TestPalindromo(t *testing.T) {
        casos := []struct {
            nome string
            s    string
            esperado bool
        }{
            {"palíndromo simples", "arara", true},
            {"com espaços", "Socorram me subi no onibus em Marrocos", true},
            {"não palíndromo", "golang", false},
        }
        for _, c := range casos {
            t.Run(c.nome, func(t *testing.T) {
                got := Palindromo(c.s)
                if got != c.esperado {
                    t.Errorf("Palindromo(%q) = %v; esperado %v", c.s, got, c.esperado)
                }
            })
        }
    }
  4. Escreva um benchmark para a função Soma que soma dois inteiros. Use a flag -bench para executá-lo e observe o resultado.

    ✓ Resposta:
    package main
    
    import "testing"
    
    func Soma(a, b int) int {
        return a + b
    }
    
    func BenchmarkSoma(b *testing.B) {
        for i := 0; i < b.N; i++ {
            Soma(1, 2)
        }
    }

    Execute com: go test -bench=.

  5. Crie um teste para uma função Fatorial(n int) (int, error) que retorna erro para n negativo ou maior que 20 (para evitar overflow). Use table-driven tests e verifique os casos de erro.

    ✓ Resposta:
    package main
    
    import (
        "errors"
        "testing"
    )
    
    func Fatorial(n int) (int, error) {
        if n < 0 || n > 20 {
            return 0, errors.New("n fora do intervalo")
        }
        resultado := 1
        for i := 2; i <= n; i++ {
            resultado *= i
        }
        return resultado, nil
    }
    
    func TestFatorial(t *testing.T) {
        casos := []struct {
            nome string
            n    int
            esperado int
            erro bool
        }{
            {"fatorial de 0", 0, 1, false},
            {"fatorial de 5", 5, 120, false},
            {"negativo", -1, 0, true},
            {"muito grande", 21, 0, true},
        }
        for _, c := range casos {
            t.Run(c.nome, func(t *testing.T) {
                got, err := Fatorial(c.n)
                if (err != nil) != c.erro {
                    t.Errorf("erro esperado? %v; obtido %v", c.erro, err)
                }
                if got != c.esperado {
                    t.Errorf("Fatorial(%d) = %d; esperado %d", c.n, got, c.esperado)
                }
            })
        }
    }