Testes Unitários Básicos em Zig

Introdução

Testes em Zig são cidadãos de primeira classe — integrados diretamente na linguagem com o bloco test. Não há necessidade de framework externo. Os testes ficam no mesmo arquivo que o código, são executados com zig test, e aproveitam o testing.allocator para detectar vazamentos de memória automaticamente.

Para testes avançados, veja Testes com Allocator, Test Expectations e Mocking e Stubbing.

Pré-requisitos

Teste Básico

const std = @import("std");

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

fn fatorial(n: u32) u64 {
    if (n == 0) return 1;
    var resultado: u64 = 1;
    var i: u32 = 1;
    while (i <= n) : (i += 1) {
        resultado *= i;
    }
    return resultado;
}

test "soma básica" {
    try std.testing.expectEqual(@as(i32, 5), soma(2, 3));
    try std.testing.expectEqual(@as(i32, 0), soma(0, 0));
    try std.testing.expectEqual(@as(i32, -1), soma(2, -3));
}

test "fatorial" {
    try std.testing.expectEqual(@as(u64, 1), fatorial(0));
    try std.testing.expectEqual(@as(u64, 1), fatorial(1));
    try std.testing.expectEqual(@as(u64, 120), fatorial(5));
    try std.testing.expectEqual(@as(u64, 3628800), fatorial(10));
}

Executar Testes

# Testar um arquivo
zig test src/main.zig

# Testar com output detalhado
zig test src/main.zig --summary all

# Testar via build system
zig build test

Assertions Disponíveis

const testing = std.testing;

test "assertions" {
    // Igualdade
    try testing.expectEqual(@as(i32, 42), resultado);

    // Strings
    try testing.expectEqualStrings("esperado", obtido);

    // Slices
    try testing.expectEqualSlices(u8, &.{ 1, 2, 3 }, slice);

    // Booleano
    try testing.expect(condicao_verdadeira);

    // Aproximação (floats)
    try testing.expectApproxEqAbs(@as(f64, 3.14), pi, 0.01);

    // Erro esperado
    try testing.expectError(error.DivisaoPorZero, dividir(1, 0));

    // Formato
    try testing.expectFmt("42", "{}", .{@as(i32, 42)});
}

Testar Funções que Retornam Erros

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

test "divisão com sucesso" {
    const resultado = try dividir(10, 2);
    try std.testing.expectEqual(@as(f64, 5.0), resultado);
}

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

Veja Error Sets Customizados para definir erros testáveis.

Organização de Testes

Testes no Mesmo Arquivo

// src/calculadora.zig

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

pub fn multiplicar(a: i32, b: i32) i32 {
    return a * b;
}

// Testes no final do arquivo
test "soma" {
    try std.testing.expectEqual(@as(i32, 7), soma(3, 4));
}

test "multiplicar" {
    try std.testing.expectEqual(@as(i32, 12), multiplicar(3, 4));
}

Testes em Arquivo Separado

// tests/calculadora_test.zig
const std = @import("std");
const calc = @import("../src/calculadora.zig");

test "soma de números negativos" {
    try std.testing.expectEqual(@as(i32, -5), calc.soma(-2, -3));
}

Bloco de Testes Agrupados

test "operações com strings" {
    // Teste 1: concatenação
    {
        // ...
    }

    // Teste 2: split
    {
        // ...
    }
}

Testes com Setup e Teardown

fn criarAmbiente(allocator: std.mem.Allocator) !*Ambiente {
    const env = try allocator.create(Ambiente);
    env.* = try Ambiente.init(allocator);
    return env;
}

test "teste com setup" {
    const allocator = std.testing.allocator;

    // Setup
    const env = try criarAmbiente(allocator);
    defer {
        env.deinit();
        allocator.destroy(env);
    }

    // Teste
    try env.executar();
    try std.testing.expect(env.resultado == .sucesso);
}

Pular Testes Condicionalmente

const builtin = @import("builtin");

test "apenas no linux" {
    if (builtin.os.tag != .linux) return error.SkipZigTest;
    // Teste específico de Linux
}

test "apenas em debug" {
    if (builtin.mode != .Debug) return error.SkipZigTest;
    // Teste que só faz sentido em debug
}

Testes com Allocator de Teste

test "função não vaza memória" {
    // testing.allocator detecta leaks automaticamente
    const resultado = try minhaFuncao(std.testing.allocator);
    defer std.testing.allocator.free(resultado);

    try std.testing.expect(resultado.len > 0);
}
// Se houver leak, o teste FALHA com mensagem clara

Veja Testes com Allocator para mais detalhes.

Integrar com build.zig

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,
    });
    b.installArtifact(exe);

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

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

Conclusão

Testes em Zig são simples, integrados e poderosos. Escreva testes junto com o código, use std.testing.allocator para detectar leaks, e execute com zig test. Para padrões mais avançados, consulte Testes com Allocator, Test Expectations e Benchmarking.

Continue aprendendo Zig

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