Unit Tests em Zig: Fundamentos do Sistema de Testes Built-in

Zig possui um dos sistemas de testes mais elegantes entre linguagens de programacao de sistemas. Testes sao cidadaos de primeira classe na linguagem — nao ha necessidade de frameworks, bibliotecas externas ou configuracoes complexas. Neste artigo, exploramos os fundamentos do sistema de testes built-in do Zig e como usa-lo efetivamente.

Para o basico de testes em Zig, confira Testes em Zig.

O Sistema de Testes do Zig

Testes em Zig sao blocos test declarados junto ao codigo. Eles sao executados com zig build test ou zig test arquivo.zig.

Primeiro Teste

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

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

test "soma basica" {
    try expect(soma(2, 3) == 5);
    try expect(soma(-1, 1) == 0);
    try expect(soma(0, 0) == 0);
}

test "soma com overflow" {
    // Zig detecta overflow em modo debug
    try expect(soma(std.math.maxInt(i32), 0) == std.math.maxInt(i32));
}

Execute com:

zig test arquivo.zig
# Saida: All 2 tests passed.

Tipos de Assertions

const std = @import("std");
const expect = std.testing.expect;
const expectEqual = std.testing.expectEqual;
const expectError = std.testing.expectError;
const expectEqualStrings = std.testing.expectEqualStrings;
const expectEqualSlices = std.testing.expectEqualSlices;
const expectApproxEqAbs = std.testing.expectApproxEqAbs;

test "tipos de assertions" {
    // Igualdade basica
    try expectEqual(@as(i32, 42), soma(40, 2));

    // Strings
    const nome = "Zig Brasil";
    try expectEqualStrings("Zig Brasil", nome);

    // Slices
    const a = [_]u8{ 1, 2, 3 };
    const b = [_]u8{ 1, 2, 3 };
    try expectEqualSlices(u8, &a, &b);

    // Floats (com tolerancia)
    try expectApproxEqAbs(@as(f64, 3.14159), @as(f64, 3.14160), 0.001);
}

Testando Erros

Um dos pontos fortes do Zig e o tratamento explicito de erros, e o sistema de testes reflete isso:

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

const MathError = error{
    DivisaoPorZero,
    Overflow,
};

fn dividir(a: i32, b: i32) MathError!i32 {
    if (b == 0) return MathError.DivisaoPorZero;
    return @divTrunc(a, b);
}

test "divisao normal" {
    const resultado = try dividir(10, 2);
    try expect(resultado == 5);
}

test "divisao por zero retorna erro" {
    try expectError(MathError.DivisaoPorZero, dividir(10, 0));
}

test "divisao com catch" {
    const resultado = dividir(10, 0) catch |err| {
        try expect(err == MathError.DivisaoPorZero);
        return;
    };
    _ = resultado;
    // Se chegou aqui, o teste deveria ter falhado
    return error.TestExpectedError;
}

Testes Parametrizados com Comptime

O comptime de Zig permite criar testes parametrizados sem macros:

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

fn fatorial(n: u64) u64 {
    if (n <= 1) return 1;
    return n * fatorial(n - 1);
}

test "fatorial parametrizado" {
    const casos = .{
        .{ 0, 1 },
        .{ 1, 1 },
        .{ 2, 2 },
        .{ 3, 6 },
        .{ 4, 24 },
        .{ 5, 120 },
        .{ 10, 3628800 },
    };

    inline for (casos) |caso| {
        const entrada = caso[0];
        const esperado = caso[1];
        try expectEqual(@as(u64, esperado), fatorial(entrada));
    }
}

Gerando Testes para Multiplos Tipos

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

fn maxGenerico(comptime T: type, a: T, b: T) T {
    return if (a > b) a else b;
}

fn testarMax(comptime T: type) !void {
    try expect(maxGenerico(T, 1, 2) == 2);
    try expect(maxGenerico(T, 5, 3) == 5);
    try expect(maxGenerico(T, 7, 7) == 7);
}

test "max para i32" {
    try testarMax(i32);
}

test "max para u64" {
    try testarMax(u64);
}

test "max para f32" {
    try testarMax(f32);
}

Allocator de Teste

O std.testing.allocator detecta memory leaks automaticamente:

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

fn duplicarString(allocator: std.mem.Allocator, str: []const u8) ![]u8 {
    const copia = try allocator.alloc(u8, str.len);
    @memcpy(copia, str);
    return copia;
}

test "duplicar string sem leak" {
    const resultado = try duplicarString(std.testing.allocator, "teste");
    defer std.testing.allocator.free(resultado);

    try std.testing.expectEqualStrings("teste", resultado);
}

// Se voce esquecer o defer free, o teste FALHA com:
// "Test leaked memory" - detectado automaticamente!

Organizando Testes em Projetos

Testes no Mesmo Arquivo

// src/calculadora.zig
pub fn soma(a: i32, b: i32) i32 {
    return a + b;
}

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

// Testes ficam no mesmo arquivo — removidos do binario de producao
test "operacoes basicas" {
    const expect = @import("std").testing.expect;
    try expect(soma(2, 3) == 5);
    try expect(multiplica(3, 4) == 12);
}

Referenciando Testes de Submodulos

// src/main.zig ou src/root.zig
const calculadora = @import("calculadora.zig");
const parser = @import("parser.zig");
const utils = @import("utils.zig");

// Forcar execucao de testes em todos os modulos
comptime {
    _ = calculadora;
    _ = parser;
    _ = utils;
}

// Testes especificos de integracao
test "integracao calculadora-parser" {
    // ...
}

Configurando no build.zig

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

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

    // 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");
    test_step.dependOn(&run_testes.step);

    b.installArtifact(exe);
}

Exercicios

  1. Stack com testes: Implemente uma stack generica com testes para push, pop, peek, isEmpty e deteccao de leak.

  2. Parser de CSV: Crie um parser de CSV simples com testes parametrizados cobrindo edge cases (campos vazios, aspas, newlines).

  3. Testes de contrato: Escreva testes que verifiquem invariantes de uma estrutura de dados (ex: BST sempre ordenada).


Proximo Artigo

No proximo artigo, exploramos padroes de teste avancados: mocks, stubs, dependency injection e table-driven tests.

Conteudo Relacionado


Duvidas sobre testes em Zig? Participe da comunidade Zig Brasil!

Continue aprendendo Zig

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