Frameworks de Teste em Zig — Testes Unitários, Integração e Benchmarks

Frameworks de Teste em Zig — Testes Unitários, Integração e Benchmarks

Uma das características mais elogiadas do Zig é seu suporte nativo e integrado a testes. Diferente da maioria das linguagens que requerem frameworks externos para testes básicos, o Zig inclui um sistema de testes robusto diretamente na linguagem e no compilador. Isso incentiva uma cultura de testes desde o primeiro dia de um projeto.

Testes Integrados na Linguagem

No Zig, testes são blocos de código especiais declarados com a keyword test diretamente nos arquivos fonte:

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

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

fn dividir(a: f64, b: f64) !f64 {
    if (b == 0) return error.DivisaoPorZero;
    return a / b;
}

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

test "somar números negativos" {
    try expect(somar(-1, -1) == -2);
}

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

test "dividir números válidos" {
    const resultado = try dividir(10, 2);
    try expect(resultado == 5.0);
}

test "dividir por zero retorna erro" {
    const resultado = dividir(10, 0);
    try std.testing.expectError(error.DivisaoPorZero, resultado);
}

Executando Testes

# Executar todos os testes do projeto
zig build test

# Executar testes de um arquivo específico
zig test src/utils.zig

# Executar com filtro de nome
zig test src/main.zig --test-filter "somar"

# Executar com verbose
zig test src/main.zig --verbose

std.testing — API de Asserções

A biblioteca padrão oferece um conjunto rico de funções de asserção:

const testing = std.testing;

test "expect básico" {
    try testing.expect(true);
    try testing.expect(1 + 1 == 2);
}

test "expectEqual para comparação de valores" {
    try testing.expectEqual(@as(i32, 42), somar(20, 22));
}

test "expectApproxEqAbs para floats" {
    try testing.expectApproxEqAbs(@as(f64, 3.14159), calculaPi(), 0.001);
}

test "expectEqualStrings para strings" {
    try testing.expectEqualStrings("olá", saudacao());
}

test "expectEqualSlices para slices" {
    const esperado = [_]u8{ 1, 2, 3, 4, 5 };
    const resultado = gerarSequencia();
    try testing.expectEqualSlices(u8, &esperado, resultado);
}

test "expectFmt para formatação" {
    try testing.expectFmt("42", "{}", .{@as(i32, 42)});
}

test "expectError para verificar erros" {
    try testing.expectError(error.InvalidInput, processar("inválido"));
}

Alocador de Teste

O std.testing.allocator detecta memory leaks automaticamente:

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

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

    try lista.append(42);
    try testing.expect(lista.items.len == 1);
}

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

    var lista = std.ArrayList(u8).init(allocator);
    try lista.append(42);
    // Esqueceu lista.deinit() — o teste falhará com erro de leak!
}

FailingAllocator para Testes de Resiliência

test "tratamento de falha de alocação" {
    var failing = std.testing.FailingAllocator.init(
        std.testing.allocator,
        .{ .fail_index = 3 }, // Falha na 4ª alocação
    );
    const allocator = failing.allocator();

    // Testar que o código trata corretamente falhas de alocação
    const resultado = minhaFuncao(allocator);
    try testing.expectError(error.OutOfMemory, resultado);
}

Testes de Integração

Separando Testes em Arquivos

Organize testes de integração em arquivos separados:

src/
├── main.zig
├── database.zig
├── http_handler.zig
tests/
├── test_database.zig
├── test_http.zig
└── test_integration.zig
// tests/test_integration.zig
const std = @import("std");
const app = @import("../src/main.zig");

test "fluxo completo de criação de usuário" {
    const allocator = std.testing.allocator;

    // Setup
    var servidor = try app.Server.init(allocator, .{ .port = 0 });
    defer servidor.deinit();

    // Executar
    const resposta = try servidor.handleRequest(.{
        .method = .POST,
        .path = "/usuarios",
        .body = "{\"nome\": \"Maria\"}",
    });

    // Verificar
    try std.testing.expectEqual(@as(u16, 201), resposta.status);
}

Configuração no build.zig

// Testes de integração no build.zig
const integration_tests = b.addTest(.{
    .root_source_file = b.path("tests/test_integration.zig"),
    .target = target,
    .optimize = optimize,
});

const run_integration = b.addRunArtifact(integration_tests);
const integration_step = b.step("test-integration", "Executar testes de integração");
integration_step.dependOn(&run_integration.step);

Benchmarks

O Zig não inclui um framework de benchmark na stdlib, mas o ecossistema oferece opções maduras:

zig-bench

const bench = @import("zig-bench");

fn benchmarkSorting(timer: *bench.Timer) void {
    var dados = gerarDadosAleatorios(10000);
    timer.start();
    std.sort.sort(u32, &dados, {}, std.sort.asc(u32));
    timer.stop();
}

pub fn main() !void {
    var b = bench.Benchmark.init(std.heap.page_allocator);
    try b.add("sorting 10k elementos", benchmarkSorting);
    try b.add("sorting 100k elementos", benchmarkSorting100k);
    try b.run();
    b.printResults();
}

Benchmark Manual com std.time

test "benchmark manual - parsing JSON" {
    const iteracoes = 10000;
    const json_input = @embedFile("test_data/grande.json");

    const inicio = std.time.nanoTimestamp();

    for (0..iteracoes) |_| {
        const parsed = try std.json.parseFromSlice(
            MeuTipo,
            std.testing.allocator,
            json_input,
            .{},
        );
        defer parsed.deinit();
    }

    const fim = std.time.nanoTimestamp();
    const duracao_ns = fim - inicio;
    const por_iteracao = @divTrunc(duracao_ns, iteracoes);

    std.debug.print("\nBenchmark: {} iterações em {}ms ({} ns/op)\n", .{
        iteracoes,
        @divTrunc(duracao_ns, 1_000_000),
        por_iteracao,
    });
}

Mocking e Test Doubles

Zig permite mocking elegante usando 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);
        }
    };
}

// Implementação real
const RealHttp = struct {
    pub fn get(_: *@This(), url: []const u8) ![]const u8 {
        _ = url;
        // Requisição HTTP real
        return "resposta real";
    }
};

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

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

test "serviço com mock HTTP" {
    var mock = MockHttp{ .resposta_mock = "{\"status\": \"ok\"}" };
    var client = HttpClient(MockHttp){ .impl = mock };
    _ = &client;

    const resultado = try client.get("/api/health");
    try std.testing.expectEqualStrings("{\"status\": \"ok\"}", resultado);
}

Boas Práticas

  1. Teste ao lado do código: Coloque test blocks no mesmo arquivo da implementação
  2. Use o testing.allocator: Detecte memory leaks automaticamente
  3. Nomeie testes descritivamente: Use nomes que descrevam o comportamento esperado
  4. Teste edge cases: Valores nulos, listas vazias, limites de overflow
  5. Automatize no CI: Execute zig build test em cada push — veja nosso guia de ferramentas de debug

Próximos Passos

Explore as ferramentas de debug para depuração avançada, as ferramentas de profiling para otimização de performance, e as ferramentas de documentação para documentar seu código testado. Consulte nossos tutoriais e receitas para exemplos práticos.

Continue aprendendo Zig

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