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
- Zig instalado (versão 0.13+). Veja o guia de instalação
- Conhecimento de testes em Zig
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
- Projete para testabilidade: Aceite dependências como parâmetros
comptimeou allocators - Prefira fakes a mocks: Implementações simples que imitam o comportamento real
- Use testing.allocator: Detecta leaks automaticamente
- Evite mocks complexos: Se precisa de muito mocking, reconsidere o design
- 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.