Testes em Zig: Guia Completo com Exemplos Práticos

Uma das decisões de design mais acertadas da linguagem Zig foi incorporar testes como cidadãos de primeira classe. Não existe framework externo, não existe configuração especial — testes vivem no mesmo arquivo que o código que testam e são executados com um único comando. Essa filosofia elimina a fricção que, em outras linguagens, faz desenvolvedores pularem testes.

Neste guia completo, vamos cobrir desde o básico até padrões avançados como fuzz testing e integração com CI/CD.

Seu Primeiro Teste em Zig

Em Zig, testes são declarados com a palavra-chave test seguida de uma string descritiva:

const std = @import("std");
const expect = std.testing.expect;

fn somar(a: i32, b: i32) i32 {
    return a + b;
}

test "somar dois números positivos" {
    const resultado = somar(2, 3);
    try expect(resultado == 5);
}

test "somar com zero" {
    try expect(somar(0, 42) == 42);
    try expect(somar(42, 0) == 42);
}

Execute com:

zig test arquivo.zig

Se todos os testes passarem, a saída será:

All 2 tests passed.

Simples assim. Sem main(), sem imports de frameworks, sem boilerplate.

Funções de Asserção

O módulo std.testing oferece várias funções de asserção:

expect e expectEqual

const testing = std.testing;

test "asserções básicas" {
    // Condição booleana
    try testing.expect(true);

    // Igualdade com mensagem de erro informativa
    try testing.expectEqual(@as(i32, 42), somar(40, 2));

    // Igualdade aproximada para floats
    try testing.expectApproxEqAbs(@as(f64, 3.14), 3.14159, 0.01);

    // Comparação de strings
    try testing.expectEqualStrings("hello", "hello");

    // Comparação de slices
    try testing.expectEqualSlices(u8, &[_]u8{ 1, 2, 3 }, &[_]u8{ 1, 2, 3 });
}

expectError: Testando Error Unions

Zig tem um sistema de error handling poderoso, e testá-lo é igualmente expressivo:

const DivisionError = error{DivisionByZero};

fn dividir(a: f64, b: f64) DivisionError!f64 {
    if (b == 0.0) return error.DivisionByZero;
    return a / b;
}

test "divisão normal" {
    const resultado = try dividir(10.0, 2.0);
    try testing.expectEqual(@as(f64, 5.0), resultado);
}

test "divisão por zero retorna erro" {
    const resultado = dividir(10.0, 0.0);
    try testing.expectError(error.DivisionByZero, resultado);
}

Detecção de Memory Leaks com Testing Allocator

Este é um dos recursos mais poderosos de testes em Zig. O std.testing.allocator detecta automaticamente memory leaks — se qualquer memória alocada não for liberada ao final do teste, o teste falha:

test "sem memory leaks" {
    const allocator = std.testing.allocator;

    var lista = std.ArrayList(u8).init(allocator);
    defer lista.deinit(); // Se esquecer isso, o teste falha!

    try lista.append(42);
    try lista.append(100);

    try testing.expectEqual(@as(usize, 2), lista.items.len);
}

test "detecta leak intencional" {
    const allocator = std.testing.allocator;

    // Isso FALHARÁ — memória alocada mas nunca liberada
    const ptr = try allocator.alloc(u8, 100);
    _ = ptr;
    // Esqueceu: allocator.free(ptr);
}

Para quem vem de linguagens com garbage collector, isso pode parecer tedioso. Mas na prática, o allocator de testes encontra bugs de memória que em C levariam horas com Valgrind. Veja nosso artigo sobre estratégias de alocação de memória para aprofundar.

Testes Table-Driven

O padrão de table-driven tests, popular em Go, é igualmente elegante em Zig:

test "fibonacci - table driven" {
    const TestCase = struct {
        input: u32,
        expected: u32,
    };

    const cases = [_]TestCase{
        .{ .input = 0, .expected = 0 },
        .{ .input = 1, .expected = 1 },
        .{ .input = 2, .expected = 1 },
        .{ .input = 5, .expected = 5 },
        .{ .input = 10, .expected = 55 },
        .{ .input = 20, .expected = 6765 },
    };

    for (cases) |tc| {
        const resultado = fibonacci(tc.input);
        try testing.expectEqual(tc.expected, resultado);
    }
}

Esse padrão é excelente para testar funções puras com múltiplas entradas. Se você conhece table-driven tests do Go, vai se sentir em casa — e pode comparar as abordagens no Go Lang Brasil.

Organização de Testes

Testes no Mesmo Arquivo

A convenção em Zig é colocar testes no mesmo arquivo que o código:

// math.zig
pub fn fatorial(n: u32) u32 {
    if (n <= 1) return 1;
    return n * fatorial(n - 1);
}

// Testes ficam no final do arquivo
test "fatorial de 0" {
    try testing.expectEqual(@as(u32, 1), fatorial(0));
}

test "fatorial de 5" {
    try testing.expectEqual(@as(u32, 120), fatorial(5));
}

Executando Testes Específicos

Use --test-filter para executar apenas testes que contenham uma substring no nome:

# Apenas testes com "fatorial" no nome
zig test math.zig --test-filter "fatorial"

# Testes de um módulo específico via build system
zig build test --test-filter "divisão"

Testes com Build System

Para projetos maiores, configure testes no build.zig:

const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    // Executável principal
    const exe = b.addExecutable(.{
        .name = "meu-projeto",
        .root_source_file = b.path("src/main.zig"),
        .target = target,
        .optimize = optimize,
    });

    // Testes unitários
    const unit_tests = b.addTest(.{
        .root_source_file = b.path("src/main.zig"),
        .target = target,
        .optimize = optimize,
    });

    const run_tests = b.addRunArtifact(unit_tests);
    const test_step = b.step("test", "Executar testes unitários");
    test_step.dependOn(&run_tests.step);

    b.installArtifact(exe);
}

Depois basta rodar:

zig build test

Testando Código Comptime

Uma particularidade de Zig: você pode testar código que roda em compile-time:

fn comptimeFibonacci(comptime n: u32) u32 {
    if (n <= 1) return n;
    return comptimeFibonacci(n - 1) + comptimeFibonacci(n - 2);
}

test "fibonacci em comptime" {
    // Este cálculo é feito inteiramente em tempo de compilação
    comptime {
        const result = comptimeFibonacci(10);
        if (result != 55) @compileError("fibonacci(10) deveria ser 55");
    }

    // Também funciona com expect normal
    try testing.expectEqual(@as(u32, 55), comptime comptimeFibonacci(10));
}

Para mais sobre comptime, veja nosso artigo dedicado: Comptime em Zig: Metaprogramação sem Macros.

Mocking e Injeção de Dependência

Zig não tem um framework de mocking embutido, mas o design da linguagem facilita injeção de dependência via parâmetros de tipo e interfaces:

// Interface via comptime
fn HttpClient(comptime Impl: type) type {
    return struct {
        impl: Impl,

        pub fn get(self: *@This(), url: []const u8) ![]const u8 {
            return self.impl.get(url);
        }
    };
}

// Mock para testes
const MockHttp = struct {
    response: []const u8,

    pub fn get(self: *MockHttp, _: []const u8) ![]const u8 {
        return self.response;
    }
};

test "fetch com mock" {
    var mock = MockHttp{ .response = "{\"status\": \"ok\"}" };
    var client = HttpClient(MockHttp){ .impl = mock };

    const response = try client.get("https://api.example.com/data");
    try testing.expectEqualStrings("{\"status\": \"ok\"}", response);
}

Esse padrão de polimorfismo em compile-time é uma das forças de Zig — sem vtables, sem overhead de runtime.

Fuzz Testing

A partir do Zig 0.12+, fuzz testing está disponível nativamente:

test "fuzz parser de número" {
    // O fuzzer gera inputs automaticamente
    try std.testing.fuzz(.{}, struct {
        fn testOne(input: []const u8) !void {
            // Tenta parsear como número
            _ = std.fmt.parseInt(i64, input, 10) catch return;
            // Se parseou, verifica que é válido
        }
    }.testOne);
}

O fuzzer gera milhares de inputs aleatórios buscando crashes, panics ou erros inesperados. É especialmente valioso para testar parsers, serializers e código que processa dados externos.

Integração com CI/CD

Para rodar testes em pipelines de CI/CD, o comando é direto:

# Exemplo com Gitea Actions
name: Test
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Setup Zig
        uses: goto-bus-stop/setup-zig@v2
        with:
          version: 0.13.0
      - name: Run tests
        run: zig build test
      - name: Run tests with release optimization
        run: zig build test -Doptimize=ReleaseSafe

Uma boa prática é rodar testes tanto em modo Debug (com todas as safety checks) quanto em ReleaseSafe (para encontrar bugs que só aparecem com otimizações).

Coverage com kcov

O Zig gera binários de teste compatíveis com ferramentas de coverage como kcov:

# Compilar testes como executável
zig test src/main.zig --test-cmd kcov --test-cmd ./coverage --test-cmd-bin

# O relatório HTML estará em ./coverage/

Isso permite integrar relatórios de cobertura de código em seu pipeline de CI.

Boas Práticas para Testes em Zig

  1. Testes perto do código: mantenha testes no mesmo arquivo — reduz contexto necessário
  2. Use testing.allocator sempre: detecta leaks automaticamente
  3. Testes devem ser rápidos: evite I/O real; use mocks para network e filesystem
  4. Testes determinísticos: nunca dependa de tempo real ou dados aleatórios (exceto fuzz)
  5. Nomes descritivos: test "multiplicação de matriz 3x3 com identidade" > test "test1"
  6. Table-driven para múltiplas entradas: evita duplicação de lógica de teste
  7. Teste error paths: use expectError — o caminho de erro é tão importante quanto o feliz
  8. Rode em múltiplos modos: Debug para safety, ReleaseSafe para otimização

Para validar código com SIMD e processamento vetorial, combine estas práticas com os exemplos do nosso artigo sobre SIMD em Zig.

Comparação com Testes em Outras Linguagens

FeatureZigRustGoPython
Testes built-intest blocks#[test]func Test*❌ unittest/pytest
Detecção de leaks✅ testing.allocator❌ (precisa Miri)N/A (GC)N/A (GC)
Fuzz built-in✅ std.testing.fuzz❌ (cargo-fuzz)testing.F❌ (hypothesis)
CoverageVia kcovVia tarpaulinVia go test -coverVia coverage.py
Tempo de compilaçãoRápidoLentoRápidoN/A

Se você vem do ecossistema Rust e quer comparar abordagens de teste, veja o Rust Lang Brasil. Para a abordagem de testing do Go, confira o Go Lang Brasil. E para quem quer comparar com pytest, visite o Python Dev Brasil.

Conclusão

O sistema de testes de Zig demonstra a filosofia da linguagem: simplicidade sem sacrificar poder. Com blocos test integrados, testing.allocator para detecção de leaks e fuzz testing nativo, você tem tudo que precisa para escrever software confiável sem dependências externas.

Comece testando funções simples com expect, evolua para table-driven tests, e depois explore fuzz testing para inputs não confiáveis. E para consultas rápidas sobre a sintaxe de testes, mantenha nosso cheatsheet de testing à mão.

Para se aprofundar em testes de propriedade em Zig, veja nosso artigo sobre testes de propriedade, e explore os frameworks de testing do ecossistema para necessidades mais avançadas.

Continue aprendendo Zig

Explore mais tutoriais e artigos em português para dominar a linguagem Zig.