std.testing em Zig — Referência e Exemplos

std.testing — Framework de Testes

O módulo std.testing é o framework de testes integrado ao Zig. Ele fornece funções de asserção, um allocator de teste que detecta leaks de memória e integração direta com o sistema de build. Testes em Zig são blocos test que vivem junto ao código-fonte e são executados com zig test.

Visão Geral

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

Testes no Zig são definidos com a keyword test e podem conter qualquer código, incluindo chamadas a funções do módulo sendo testado. O runner de testes executa cada bloco test isoladamente.

Funções de Asserção Principais

// Verifica se expressão é verdadeira
pub fn expect(ok: bool) !void

// Verifica igualdade de valores
pub fn expectEqual(expected: anytype, actual: anytype) !void

// Verifica igualdade de slices
pub fn expectEqualSlices(comptime T: type, expected: []const T, actual: []const T) !void

// Verifica igualdade de strings
pub fn expectEqualStrings(expected: []const u8, actual: []const u8) !void

// Verifica que valor é aproximadamente igual (floats)
pub fn expectApproxEqAbs(comptime T: type, expected: T, actual: T, tolerance: T) !void
pub fn expectApproxEqRel(comptime T: type, expected: T, actual: T, tolerance: T) !void

// Verifica que valor é null/não-null
pub fn expectEqual(expected: @TypeOf(null), actual: anytype) !void

// Verifica que uma expressão retorna erro
pub fn expectError(expected: anyerror, actual: anytype) !void

// Allocator de teste (detecta memory leaks)
pub const allocator: std.mem.Allocator = ...;

Exemplo 1: Testes Básicos

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

/// Calcula o fatorial de n.
fn fatorial(n: u64) u64 {
    if (n == 0) return 1;
    var resultado: u64 = 1;
    for (1..n + 1) |i| {
        resultado *= i;
    }
    return resultado;
}

/// Verifica se uma string é palíndromo.
fn ehPalindromo(s: []const u8) bool {
    if (s.len <= 1) return true;
    var i: usize = 0;
    var j: usize = s.len - 1;
    while (i < j) {
        if (s[i] != s[j]) return false;
        i += 1;
        j -= 1;
    }
    return true;
}

/// Inverte uma string in-place.
fn inverter(buf: []u8) void {
    if (buf.len <= 1) return;
    var i: usize = 0;
    var j: usize = buf.len - 1;
    while (i < j) {
        const temp = buf[i];
        buf[i] = buf[j];
        buf[j] = temp;
        i += 1;
        j -= 1;
    }
}

test "fatorial de casos base" {
    try testing.expectEqual(@as(u64, 1), fatorial(0));
    try testing.expectEqual(@as(u64, 1), fatorial(1));
}

test "fatorial de valores conhecidos" {
    try testing.expectEqual(@as(u64, 120), fatorial(5));
    try testing.expectEqual(@as(u64, 3628800), fatorial(10));
    try testing.expectEqual(@as(u64, 2432902008176640000), fatorial(20));
}

test "palindromo" {
    try testing.expect(ehPalindromo("aba"));
    try testing.expect(ehPalindromo("abba"));
    try testing.expect(ehPalindromo("a"));
    try testing.expect(ehPalindromo(""));
    try testing.expect(!ehPalindromo("abc"));
    try testing.expect(!ehPalindromo("ab"));
}

test "inverter string" {
    var buf = "abcdef".*;
    inverter(&buf);
    try testing.expectEqualStrings("fedcba", &buf);

    var buf2 = "a".*;
    inverter(&buf2);
    try testing.expectEqualStrings("a", &buf2);
}

Exemplo 2: Testando com Allocator de Teste

const std = @import("std");
const testing = std.testing;
const Allocator = std.mem.Allocator;

/// Lista dinâmica simplificada.
fn Lista(comptime T: type) type {
    return struct {
        const Self = @This();

        items: []T,
        capacidade: usize,
        tamanho: usize,
        allocator: Allocator,

        pub fn init(allocator: Allocator) Self {
            return .{
                .items = &[_]T{},
                .capacidade = 0,
                .tamanho = 0,
                .allocator = allocator,
            };
        }

        pub fn deinit(self: *Self) void {
            if (self.capacidade > 0) {
                self.allocator.free(self.items.ptr[0..self.capacidade]);
            }
        }

        pub fn adicionar(self: *Self, item: T) !void {
            if (self.tamanho >= self.capacidade) {
                try self.crescer();
            }
            self.items.ptr[self.tamanho] = item;
            self.tamanho += 1;
            self.items.len = self.tamanho;
        }

        pub fn obter(self: *const Self, indice: usize) ?T {
            if (indice >= self.tamanho) return null;
            return self.items[indice];
        }

        fn crescer(self: *Self) !void {
            const nova_cap = if (self.capacidade == 0) 4 else self.capacidade * 2;
            if (self.capacidade > 0) {
                const novo = try self.allocator.realloc(self.items.ptr[0..self.capacidade], nova_cap);
                self.items = novo[0..self.tamanho];
            } else {
                const novo = try self.allocator.alloc(T, nova_cap);
                self.items = novo[0..0];
            }
            self.capacidade = nova_cap;
        }
    };
}

test "Lista — operações básicas" {
    // testing.allocator detecta memory leaks automaticamente!
    var lista = Lista(i32).init(testing.allocator);
    defer lista.deinit(); // Sem defer, o teste FALHA por leak

    try lista.adicionar(10);
    try lista.adicionar(20);
    try lista.adicionar(30);

    try testing.expectEqual(@as(usize, 3), lista.tamanho);
    try testing.expectEqual(@as(?i32, 10), lista.obter(0));
    try testing.expectEqual(@as(?i32, 20), lista.obter(1));
    try testing.expectEqual(@as(?i32, 30), lista.obter(2));
    try testing.expectEqual(@as(?i32, null), lista.obter(3));
}

test "Lista — crescimento dinâmico" {
    var lista = Lista(u8).init(testing.allocator);
    defer lista.deinit();

    // Adiciona mais que a capacidade inicial (4)
    for (0..100) |i| {
        try lista.adicionar(@intCast(i));
    }

    try testing.expectEqual(@as(usize, 100), lista.tamanho);
    try testing.expectEqual(@as(?u8, 0), lista.obter(0));
    try testing.expectEqual(@as(?u8, 99), lista.obter(99));
}

Exemplo 3: Testes de Erro e Floats

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

const ErroMatematico = error{
    DivisaoPorZero,
    Overflow,
    RaizNegativa,
};

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

fn raizQuadrada(x: f64) ErroMatematico!f64 {
    if (x < 0.0) return ErroMatematico.RaizNegativa;
    return @sqrt(x);
}

test "divisão normal" {
    const resultado = try dividir(10.0, 3.0);
    try testing.expectApproxEqAbs(3.333333, resultado, 0.001);
}

test "divisão por zero retorna erro" {
    try testing.expectError(ErroMatematico.DivisaoPorZero, dividir(5.0, 0.0));
}

test "raiz quadrada de positivo" {
    const resultado = try raizQuadrada(16.0);
    try testing.expectEqual(@as(f64, 4.0), resultado);
}

test "raiz quadrada de negativo retorna erro" {
    try testing.expectError(ErroMatematico.RaizNegativa, raizQuadrada(-1.0));
}

test "comparação de floats com tolerância relativa" {
    const pi = 3.14159265358979;
    const pi_aprox = 3.14;
    try testing.expectApproxEqRel(pi, pi_aprox, 0.001);
}

test "comparação de slices" {
    const esperado = [_]i32{ 1, 2, 3, 4, 5 };
    var resultado = [_]i32{ 5, 4, 3, 2, 1 };
    std.mem.sort(i32, &resultado, {}, std.sort.asc(i32));
    try testing.expectEqualSlices(i32, &esperado, &resultado);
}

Exemplo 4: Organizando Testes em Módulos

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

/// Módulo de utilidades de string.
const StringUtils = struct {
    /// Conta ocorrências de um caractere.
    pub fn contarCaractere(s: []const u8, c: u8) usize {
        var count: usize = 0;
        for (s) |ch| {
            if (ch == c) count += 1;
        }
        return count;
    }

    /// Verifica se string contém apenas dígitos.
    pub fn apenasDigitos(s: []const u8) bool {
        if (s.len == 0) return false;
        for (s) |c| {
            if (c < '0' or c > '9') return false;
        }
        return true;
    }

    /// Capitaliza primeira letra.
    pub fn capitalizar(buf: []u8) void {
        if (buf.len > 0 and buf[0] >= 'a' and buf[0] <= 'z') {
            buf[0] -= 32;
        }
    }
};

// Importa testes de submódulos
test {
    // Referencia todos os testes em arquivos importados
    _ = StringUtils;
}

test "contarCaractere" {
    try testing.expectEqual(@as(usize, 3), StringUtils.contarCaractere("banana", 'a'));
    try testing.expectEqual(@as(usize, 0), StringUtils.contarCaractere("xyz", 'a'));
    try testing.expectEqual(@as(usize, 0), StringUtils.contarCaractere("", 'a'));
}

test "apenasDigitos" {
    try testing.expect(StringUtils.apenasDigitos("12345"));
    try testing.expect(!StringUtils.apenasDigitos("123a5"));
    try testing.expect(!StringUtils.apenasDigitos(""));
}

test "capitalizar" {
    var buf = "zig".*;
    StringUtils.capitalizar(&buf);
    try testing.expectEqualStrings("Zig", &buf);

    var buf2 = "Zig".*;
    StringUtils.capitalizar(&buf2);
    try testing.expectEqualStrings("Zig", &buf2);
}

Executando Testes

# Executar todos os testes do arquivo
zig test meu_arquivo.zig

# Com saída detalhada
zig test meu_arquivo.zig --summary all

# Filtrar testes por nome
zig test meu_arquivo.zig --test-filter "fatorial"

Módulos Relacionados

Tutoriais Relacionados

Continue aprendendo Zig

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