Mocking e Stubbing em Zig

Introdução

Zig não tem framework de mocking integrado como Mockito (Java) ou unittest.mock (Python). Em vez disso, a linguagem encoraja design que facilita testes naturalmente — via comptime generics e allocators injetáveis. Esta receita mostra como aplicar esses padrões na prática.

Para testes básicos, veja Testes Unitários Básicos. Para testes com allocators, consulte Testes com Allocator.

Pré-requisitos

Padrão 1: Injeção de Dependência via Comptime

Em vez de criar mocks complexos, projete seu código para aceitar dependências como tipos comptime:

const std = @import("std");

// Código de produção: aceita qualquer tipo com interface read/write
pub fn Processador(comptime Storage: type) type {
    return struct {
        storage: Storage,

        const Self = @This();

        pub fn processar(self: *Self, dados: []const u8) !void {
            try self.storage.write(dados);
        }

        pub fn ler(self: *Self, buffer: []u8) !usize {
            return self.storage.read(buffer);
        }
    };
}

// Implementação real
const FileStorage = struct {
    file: std.fs.File,

    pub fn write(self: *FileStorage, dados: []const u8) !void {
        try self.file.writeAll(dados);
    }

    pub fn read(self: *FileStorage, buffer: []u8) !usize {
        return self.file.read(buffer);
    }
};

// Stub para testes
const StubStorage = struct {
    escrito: std.ArrayList(u8),
    dados_para_ler: []const u8,
    pos_leitura: usize,

    pub fn init(allocator: std.mem.Allocator, dados: []const u8) StubStorage {
        return .{
            .escrito = std.ArrayList(u8).init(allocator),
            .dados_para_ler = dados,
            .pos_leitura = 0,
        };
    }

    pub fn deinit(self: *StubStorage) void {
        self.escrito.deinit();
    }

    pub fn write(self: *StubStorage, dados: []const u8) !void {
        try self.escrito.appendSlice(dados);
    }

    pub fn read(self: *StubStorage, buffer: []u8) !usize {
        const restante = self.dados_para_ler[self.pos_leitura..];
        const n = @min(buffer.len, restante.len);
        @memcpy(buffer[0..n], restante[0..n]);
        self.pos_leitura += n;
        return n;
    }
};

test "processador com stub storage" {
    const allocator = std.testing.allocator;

    var storage = StubStorage.init(allocator, "dados de teste");
    defer storage.deinit();

    var proc = Processador(StubStorage){ .storage = storage };

    try proc.processar("Olá");
    try std.testing.expectEqualStrings("Olá", proc.storage.escrito.items);
}

Padrão 2: Fake com Interface de Ponteiro

Para polimorfismo dinâmico quando comptime não é prático:

const HttpClient = struct {
    ptr: *anyopaque,
    getFn: *const fn (*anyopaque, []const u8) anyerror![]const u8,

    pub fn get(self: HttpClient, url: []const u8) ![]const u8 {
        return self.getFn(self.ptr, url);
    }
};

// Fake para testes
const FakeHttpClient = struct {
    respostas: std.StringHashMap([]const u8),
    allocator: std.mem.Allocator,

    pub fn init(allocator: std.mem.Allocator) FakeHttpClient {
        return .{
            .respostas = std.StringHashMap([]const u8).init(allocator),
            .allocator = allocator,
        };
    }

    pub fn deinit(self: *FakeHttpClient) void {
        self.respostas.deinit();
    }

    pub fn registrar(self: *FakeHttpClient, url: []const u8, resposta: []const u8) !void {
        try self.respostas.put(url, resposta);
    }

    pub fn asClient(self: *FakeHttpClient) HttpClient {
        return .{
            .ptr = @ptrCast(self),
            .getFn = @ptrCast(&get),
        };
    }

    fn get(self: *FakeHttpClient, url: []const u8) anyerror![]const u8 {
        return self.respostas.get(url) orelse return error.NaoEncontrado;
    }
};

test "fake http client" {
    const allocator = std.testing.allocator;

    var fake = FakeHttpClient.init(allocator);
    defer fake.deinit();

    try fake.registrar("/api/status", "ok");

    const client = fake.asClient();
    const resposta = try client.get("/api/status");
    try std.testing.expectEqualStrings("ok", resposta);
}

Padrão 3: Spy (Registrar Chamadas)

fn SpyLogger(comptime LoggerReal: type) type {
    return struct {
        real: LoggerReal,
        chamadas: std.ArrayList([]const u8),

        const Self = @This();

        pub fn init(allocator: std.mem.Allocator, real: LoggerReal) Self {
            return .{
                .real = real,
                .chamadas = std.ArrayList([]const u8).init(allocator),
            };
        }

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

        pub fn log(self: *Self, msg: []const u8) !void {
            try self.chamadas.append(msg);
            self.real.log(msg);
        }

        pub fn foiChamadoCom(self: Self, msg: []const u8) bool {
            for (self.chamadas.items) |chamada| {
                if (std.mem.eql(u8, chamada, msg)) return true;
            }
            return false;
        }

        pub fn numeroDeChamadas(self: Self) usize {
            return self.chamadas.items.len;
        }
    };
}

Padrão 4: Allocator como Mock

O allocator de teste já funciona como um “mock” de memória:

test "verifica que função libera tudo" {
    // testing.allocator é efetivamente um mock que verifica leaks
    var cache = try Cache.init(std.testing.allocator);
    defer cache.deinit();

    try cache.put("chave", "valor");
    try cache.put("outra", "coisa");
    cache.limpar();

    try std.testing.expectEqual(@as(usize, 0), cache.tamanho());
}

test "failing allocator para testar erro de memória" {
    // FailingAllocator: simula falha de alocação
    var fba_buffer: [64]u8 = undefined;
    var fba = std.heap.FixedBufferAllocator.init(&fba_buffer);

    const resultado = MinhaStruct.init(fba.allocator());
    // Se a struct precisa de mais que 64 bytes, falha com OutOfMemory
    try std.testing.expectError(error.OutOfMemory, resultado);
}

Padrão 5: Stub de Função

fn SistemaArquivos(comptime leitorFn: fn ([]const u8) anyerror![]const u8) type {
    return struct {
        pub fn carregarConfig() !Config {
            const dados = try leitorFn("config.json");
            return Config.parse(dados);
        }
    };
}

fn lerArquivoReal(caminho: []const u8) ![]const u8 {
    _ = caminho;
    // leitura real do filesystem
    return "";
}

fn lerArquivoStub(caminho: []const u8) ![]const u8 {
    _ = caminho;
    return "{\"porta\": 8080}";
}

// Produção
const SistemaProd = SistemaArquivos(lerArquivoReal);

// Teste
const SistemaTeste = SistemaArquivos(lerArquivoStub);

test "carregar config com stub" {
    const config = try SistemaTeste.carregarConfig();
    try std.testing.expectEqual(@as(u16, 8080), config.porta);
}

Boas Práticas

  1. Projete para testabilidade: Aceite dependências como parâmetros comptime ou allocators
  2. Prefira fakes a mocks: Implementações simples que imitam o comportamento real
  3. Use testing.allocator: Detecta leaks automaticamente
  4. Evite mocks complexos: Se precisa de muito mocking, reconsidere o design
  5. Teste comportamento, não implementação: Verifique resultados, não chamadas internas

Conclusão

Mocking em Zig é feito via design — injeção de dependência com comptime generics, allocators substituíveis, e fakes simples. Essa abordagem resulta em código mais testável e menos dependente de frameworks externos.

Para mais sobre testes, veja Testes Unitários Básicos, Testes com Allocator e Benchmarking.

Continue aprendendo Zig

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