1. Introdução ao Teste de Unidade

O teste de unidade refere-se à verificação e validação da menor unidade testável em um programa, como uma função ou método na linguagem Go. O teste de unidade garante que o código funcione como esperado e permite que os desenvolvedores façam alterações no código sem quebrar acidentalmente a funcionalidade existente.

Em um projeto Go, a importância do teste de unidade é inquestionável. Em primeiro lugar, pode melhorar a qualidade do código, dando aos desenvolvedores mais confiança para fazer alterações no código. Em segundo lugar, o teste de unidade pode servir como documentação para o código, explicando seu comportamento esperado. Além disso, a execução automática de testes de unidade em um ambiente de integração contínua pode descobrir rapidamente bugs recém-introduzidos, melhorando assim a estabilidade do software.

2. Realizando Testes Básicos Usando o Pacote testing

A biblioteca padrão da linguagem Go inclui o pacote testing, que fornece ferramentas e funcionalidades para escrever e executar testes.

2.1 Criando Seu Primeiro Caso de Teste

Para escrever uma função de teste, é necessário criar um arquivo com o sufixo _test.go. Por exemplo, se o arquivo de código fonte se chama calculator.go, o arquivo de teste deve ser nomeado calculator_test.go.

Em seguida, é hora de criar a função de teste. Uma função de teste precisa importar o pacote testing e seguir um formato específico. Aqui está um exemplo simples:

// calculator_test.go
package calculator

import (
	"testing"
	"fmt"
)

// Testar a função de adição
func TestAdd(t *testing.T) {
	resultado := Add(1, 2)
	esperado := 3

if resultado != esperado {
		t.Errorf("Esperado %v, mas obteve %v", esperado, resultado)
	}
}

Neste exemplo, TestAdd é uma função de teste que testa uma função imaginária Add. Se o resultado da função Add corresponder ao resultado esperado, o teste será aprovado; caso contrário, t.Errorf será chamado para registrar as informações sobre a falha no teste.

2.2 Compreensão das Regras de Nomenclatura e Assinatura das Funções de Teste

As funções de teste devem começar com Test, seguido por qualquer string que não seja minúscula, e seu único parâmetro deve ser um ponteiro para testing.T. Como mostrado no exemplo, TestAdd segue as regras de nomenclatura e assinatura corretas.

2.3 Executando Casos de Teste

É possível executar os casos de teste usando a ferramenta de linha de comando. Para um caso de teste específico, execute o seguinte comando:

go test -v // Executar testes no diretório atual e exibir saída detalhada

Se desejar executar um caso de teste específico, pode-se usar a flag -run seguido de uma expressão regular:

go test -v -run TestAdd // Executar apenas a função de teste TestAdd

O comando go test encontrará automaticamente todos os arquivos _test.go e executará todas as funções de teste que atenderem aos critérios. Se todos os testes passarem, será exibida uma mensagem semelhante a PASS na linha de comando; se algum teste falhar, será exibida a mensagem FAIL juntamente com a mensagem de erro correspondente.

3. Escrevendo Casos de Teste

3.1 Relatando Erros Usando t.Errorf e t.Fatalf

Na linguagem Go, o framework de teste fornece vários métodos para relatar erros. As duas funções mais comumente usadas são Errorf e Fatalf, ambas são métodos do objeto testing.T. Errorf é usado para relatar erros no teste, mas não interrompe o caso de teste atual, enquanto Fatalf interrompe o teste imediatamente após relatar um erro. É importante escolher o método apropriado com base nos requisitos de teste.

Exemplo de uso de Errorf:

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

Se desejar interromper imediatamente o teste ao detectar um erro, pode-se usar Fatalf:

func TestSubtract(t *testing.T) {
    obtido := Subtract(5, 3)
    if obtido != 2 {
        t.Fatalf("Subtract(5, 3) = %d; esperado 2", obtido)
    }
}

Em geral, se o erro causaria a subsequente execução incorreta do código ou a falha no teste puder ser confirmada antecipadamente, é recomendável usar Fatalf. Caso contrário, é recomendável usar Errorf para obter um resultado de teste mais abrangente.

3.2 Organização de Subtestes e Execução de Subtestes

Em Go, podemos usar t.Run para organizar subtestes, o que nos ajuda a escrever código de teste de maneira mais estruturada. Os subtestes podem ter seu próprio Setup e Teardown e podem ser executados individualmente, proporcionando grande flexibilidade. Isso é especialmente útil para realizar testes complexos ou testes parametrizados.

Exemplo de uso do subteste t.Run:

func TestMultiply(t *testing.T) {
    testcases := []struct {
        name           string
        a, b, expected int
    }{
        {"2x3", 2, 3, 6},
        {"-1x-1", -1, -1, 1},
        {"0x4", 0, 4, 0},
    }

    for _, tc := range testcases {
        t.Run(tc.name, func(t *testing.T) {
            if got := Multiply(tc.a, tc.b); got != tc.expected {
                t.Errorf("Multiply(%d, %d) = %d; esperado %d", tc.a, tc.b, got, tc.expected)
            }
        })
    }
}

Se quisermos executar o subteste chamado "2x3" individualmente, podemos executar o seguinte comando na linha de comando:

go test -run TestMultiply/2x3

Por favor, note que os nomes dos subtestes fazem distinção entre maiúsculas e minúsculas.

4. Preparação Antes e Depois dos Testes

4.1 Setup e Teardown

Ao conduzir testes, frequentemente precisamos preparar algum estado inicial para os testes (como conexão com banco de dados, criação de arquivos, etc.) e, da mesma forma, precisamos fazer algum trabalho de limpeza após a conclusão dos testes. Em Go, geralmente realizamos Setup e Teardown diretamente nas funções de teste, e a função t.Cleanup nos fornece a capacidade de registrar funções de retorno de chamada de limpeza.

Aqui está um exemplo simples:

func TestDatabase(t *testing.T) {
    db, err := SetupDatabase()
    if err != nil {
        t.Fatalf("configuração falhou: %v", err)
    }

    // Registrar um retorno de chamada de limpeza para garantir que a conexão com o banco de dados seja fechada quando o teste for concluído
    t.Cleanup(func() {
        if err := db.Close(); err != nil {
            t.Errorf("falha ao fechar o banco de dados: %v", err)
        }
    })

    // Realizar o teste...
}

Na função TestDatabase, primeiro chamamos a função SetupDatabase para configurar o ambiente de teste. Em seguida, usamos t.Cleanup() para registrar uma função que será chamada após a conclusão do teste para realizar o trabalho de limpeza, neste exemplo, é fechar a conexão com o banco de dados. Dessa forma, podemos garantir que os recursos sejam liberados corretamente, independentemente de o teste ser bem-sucedido ou falhar.

5. Melhorando a Eficiência do Teste

Melhorar a eficiência do teste pode nos ajudar a iterar o desenvolvimento mais rapidamente, descobrir problemas rapidamente e garantir a qualidade do código. Abaixo, discutiremos a cobertura de teste, testes baseados em tabelas e o uso de simulacros para melhorar a eficiência do teste.

5.1 Cobertura de Teste e Ferramentas Relacionadas

A ferramenta go test fornece um recurso muito útil de cobertura de teste, que nos ajuda a entender quais partes do código são cobertas pelos casos de teste, descobrindo assim as áreas do código que não são cobertas pelos casos de teste.

Usando o comando go test -cover, é possível ver a porcentagem atual de cobertura de teste:

go test -cover

Se você deseja entender mais detalhadamente quais linhas de código foram executadas e quais não, pode usar o parâmetro -coverprofile, que gera um arquivo de dados de cobertura. Em seguida, é possível utilizar o comando go tool cover para gerar um relatório detalhado de cobertura de teste.

go test -coverprofile=coverage.out
go tool cover -html=coverage.out

O comando acima abrirá um relatório da web, mostrando visualmente quais linhas de código são cobertas por testes e quais não são. O verde representa código testado, enquanto o vermelho representa linhas de código não cobertas.

5.2 Utilizando Mocks

Nos testes, frequentemente nos deparamos com cenários em que precisamos simular dependências externas. Mocks podem nos ajudar a simular essas dependências, eliminando a necessidade de depender de serviços externos específicos ou recursos no ambiente de teste.

Existem muitas ferramentas de mock na comunidade Go, como testify/mock e gomock. Normalmente, essas ferramentas fornecem uma série de APIs para criar e usar objetos de mock.

Aqui está um exemplo básico de uso do testify/mock. A primeira coisa a fazer é definir uma interface e sua versão de mock:

type DataService interface {
    FetchData() (int, error)
}

type MockDataService struct {
    mock.Mock
}

func (m *MockDataService) FetchData() (int, error) {
    args := m.Called()
    return args.Int(0), args.Error(1)
}

Nos testes, podemos usar MockDataService para substituir o serviço de dados real:

func TestSomething(t *testing.T) {
    mockDataSvc := new(MockDataService)
    mockDataSvc.On("FetchData").Return(42, nil) // Configurando o comportamento esperado

    resultado, err := mockDataSvc.FetchData() // Usando o objeto de mock
    assert.NoError(t, err)
    assert.Equal(t, 42, resultado)

    mockDataSvc.AssertExpectations(t) // Verificando se o comportamento esperado ocorreu
}

Através da abordagem acima, podemos evitar depender ou isolar serviços externos, chamadas de banco de dados, etc., nos testes. Isso pode acelerar a execução do teste e tornar nossos testes mais estáveis e confiáveis.

6. Técnicas Avançadas de Testes

Após dominar o básico dos testes de unidade em Go, podemos explorar ainda mais algumas técnicas de teste avançadas, que ajudam a construir software mais robusto e a melhorar a eficiência dos testes.

6.1 Testando Funções Privadas

Em Golang, funções privadas normalmente se referem a funções não exportadas, ou seja, funções cujos nomes começam com uma letra minúscula. Geralmente, preferimos testar interfaces públicas, pois refletem a usabilidade do código. No entanto, existem casos em que testar diretamente funções privadas também faz sentido, como quando a função privada possui lógica complexa e é chamada por múltiplas funções públicas.

Testar funções privadas difere de testar funções públicas porque não podem ser acessadas de fora do pacote. Uma técnica comum é escrever o código de teste no mesmo pacote, permitindo acesso à função privada.

Aqui está um exemplo simples:

// calculator.go
package calculator

func add(a, b int) int {
    return a + b
}

O arquivo de teste correspondente é o seguinte:

// calculator_test.go
package calculator

import "testing"

func TestAdd(t *testing.T) {
    esperado := 4
    atual := add(2, 2)
    if atual != esperado {
        t.Errorf("esperado %d, obteve %d", esperado, atual)
    }
}

Ao colocar o arquivo de teste no mesmo pacote, podemos testar diretamente a função add.

6.2 Padrões Comuns de Testes e Melhores Práticas

Os testes de unidade em Golang apresentam alguns padrões comuns que facilitam o trabalho de teste e ajudam a manter a clareza e a manutenibilidade do código.

  1. Testes Baseados em Tabelas

    Testes baseados em tabelas é um método de organização de entradas de teste e saídas esperadas. Ao definir um conjunto de casos de teste e depois iterar por eles para teste, este método facilita muito a adição de novos casos de teste e também torna o código mais fácil de ler e manter.

    // calculator_test.go
    package calculator

    import "testing"

    func TestAddTableDriven(t *testing.T) {
        var testes = []struct {
            a, b   int
            esperado   int
        }{
            {1, 2, 3},
            {2, 2, 4},
            {5, -1, 4},
        }

        for _, tt := range testes {
            nomeDoTeste := fmt.Sprintf("%d,%d", tt.a, tt.b)
            t.Run(nomeDoTeste, func(t *testing.T) {
                resultado := add(tt.a, tt.b)
                if tt.esperado != resultado {
                    t.Errorf("obteve %d, esperado %d", resultado, tt.esperado)
                }
            })
        }
    }
  1. Utilizando Mocks para Testes

    Mocking é uma técnica de teste que envolve a substituição de dependências para testar várias partes da funcionalidade. Em Golang, as interfaces são a principal maneira de implementar mocks. Ao usar interfaces, uma implementação de mock pode ser criada e depois usada nos testes.