Testes com Allocator em Zig

Introdução

Uma das maiores vantagens de testar em Zig é o std.testing.allocator — um allocator especial que detecta vazamentos de memória automaticamente. Se seu código esquece de liberar memória, o teste falha com uma mensagem clara. Esta receita mostra como aproveitar isso ao máximo.

Para testes básicos, veja Testes Unitários Básicos. Para detecção de leaks em produção, consulte Detectar Vazamentos de Memória.

Pré-requisitos

O testing.allocator

O std.testing.allocator é um allocator que rastreia todas as alocações e verifica, ao final do teste, que todas foram liberadas:

const std = @import("std");

fn criarMensagem(allocator: std.mem.Allocator, nome: []const u8) ![]u8 {
    return std.fmt.allocPrint(allocator, "Olá, {s}!", .{nome});
}

test "criarMensagem libera corretamente" {
    const msg = try criarMensagem(std.testing.allocator, "Zig");
    defer std.testing.allocator.free(msg);

    try std.testing.expectEqualStrings("Olá, Zig!", msg);
}

test "ESTE TESTE FALHA - leak detectado" {
    const msg = try criarMensagem(std.testing.allocator, "Zig");
    // BUG: esqueceu de fazer free!
    // O teste FALHA automaticamente com:
    // "memory leak detected"
    _ = msg;
}

Testar Structs com Allocator

const Lista = struct {
    items: std.ArrayList(i32),

    pub fn init(allocator: std.mem.Allocator) Lista {
        return .{ .items = std.ArrayList(i32).init(allocator) };
    }

    pub fn deinit(self: *Lista) void {
        self.items.deinit();
    }

    pub fn adicionar(self: *Lista, valor: i32) !void {
        try self.items.append(valor);
    }

    pub fn soma(self: Lista) i64 {
        var total: i64 = 0;
        for (self.items.items) |item| {
            total += item;
        }
        return total;
    }
};

test "Lista - operações básicas" {
    var lista = Lista.init(std.testing.allocator);
    defer lista.deinit(); // ESSENCIAL: evita leak

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

    try std.testing.expectEqual(@as(i64, 60), lista.soma());
}

test "Lista - vazia" {
    var lista = Lista.init(std.testing.allocator);
    defer lista.deinit();

    try std.testing.expectEqual(@as(i64, 0), lista.soma());
}

Testar Funções que Transferem Ownership

fn duplicarArray(allocator: std.mem.Allocator, dados: []const u8) ![]u8 {
    return allocator.dupe(u8, dados);
}

test "duplicar array - ownership transferida" {
    const original = "dados de teste";
    const copia = try duplicarArray(std.testing.allocator, original);
    defer std.testing.allocator.free(copia); // Chamador libera

    try std.testing.expectEqualStrings(original, copia);
    try std.testing.expect(original.ptr != copia.ptr); // São cópias distintas
}

Testar com Arena Allocator

Para testes que fazem muitas alocações temporárias:

test "processamento complexo com arena" {
    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
    defer arena.deinit(); // Libera TUDO de uma vez

    const alloc = arena.allocator();

    // Muitas alocações sem precisar de free individual
    const a = try alloc.alloc(u8, 100);
    const b = try alloc.alloc(u8, 200);
    const c = try std.fmt.allocPrint(alloc, "{s}{s}", .{ a[0..5], b[0..5] });

    try std.testing.expect(c.len == 10);
    // arena.deinit() no defer cuida de tudo
}

Veja ArenaAllocator para mais detalhes.

Testar que Errdefer Funciona

fn criarRecurso(allocator: std.mem.Allocator, falhar: bool) !*Recurso {
    const r = try allocator.create(Recurso);
    errdefer allocator.destroy(r);

    r.* = .{ .dados = try allocator.alloc(u8, 100) };
    errdefer allocator.free(r.dados);

    if (falhar) return error.Falha;

    return r;
}

test "errdefer libera tudo em caso de erro" {
    // Este teste verifica que NÃO há leak quando a função falha
    const resultado = criarRecurso(std.testing.allocator, true);
    try std.testing.expectError(error.Falha, resultado);
    // Se errdefer não funcionasse, testing.allocator detectaria leak
}

test "sucesso não aciona errdefer" {
    const r = try criarRecurso(std.testing.allocator, false);
    defer {
        std.testing.allocator.free(r.dados);
        std.testing.allocator.destroy(r);
    }
    try std.testing.expect(r.dados.len == 100);
}

Veja Padrões Errdefer.

Testar com FixedBufferAllocator

Para testes que precisam ser determinísticos ou sem heap:

test "operação cabe em buffer fixo" {
    var buffer: [1024]u8 = undefined;
    var fba = std.heap.FixedBufferAllocator.init(&buffer);
    const alloc = fba.allocator();

    const dados = try alloc.alloc(u8, 100);
    try std.testing.expectEqual(@as(usize, 100), dados.len);

    // Testar que a operação não excede o buffer
    alloc.free(dados);
}

test "detectar uso excessivo de memória" {
    var buffer: [64]u8 = undefined;
    var fba = std.heap.FixedBufferAllocator.init(&buffer);
    const alloc = fba.allocator();

    // Deve falhar se tentar alocar mais que 64 bytes
    const resultado = alloc.alloc(u8, 128);
    try std.testing.expectError(error.OutOfMemory, resultado);
}

Veja FixedBufferAllocator.

Padrão: Helper de Teste

fn TestContext(comptime T: type) type {
    return struct {
        allocator: std.mem.Allocator,
        instancia: T,

        const Self = @This();

        pub fn init() !Self {
            const allocator = std.testing.allocator;
            return .{
                .allocator = allocator,
                .instancia = try T.init(allocator),
            };
        }

        pub fn deinit(self: *Self) void {
            self.instancia.deinit();
        }
    };
}

test "usando test context" {
    var ctx = try TestContext(MinhaStruct).init();
    defer ctx.deinit();

    try ctx.instancia.operacao();
}

Boas Práticas

  1. Sempre use std.testing.allocator em testes que alocam memória
  2. Sempre use defer deinit() para structs com resources
  3. Teste tanto sucesso quanto falha para verificar que errdefer funciona
  4. Use arena em testes complexos para simplificar cleanup
  5. Verifique leaks propositalmente para garantir que detecção funciona

Conclusão

O testing.allocator é uma ferramenta essencial para qualidade de código Zig. Ele transforma leaks de memória de bugs silenciosos em falhas de teste explícitas. Use-o em todos os testes que envolvem alocação de memória.

Para mais, veja Testes Unitários Básicos, Test Expectations e Mocking e Stubbing.

Continue aprendendo Zig

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