Frameworks de Teste em Zig — Testes Unitários, Integração e Benchmarks
Uma das características mais elogiadas do Zig é seu suporte nativo e integrado a testes. Diferente da maioria das linguagens que requerem frameworks externos para testes básicos, o Zig inclui um sistema de testes robusto diretamente na linguagem e no compilador. Isso incentiva uma cultura de testes desde o primeiro dia de um projeto.
Testes Integrados na Linguagem
No Zig, testes são blocos de código especiais declarados com a keyword test diretamente nos arquivos fonte:
const std = @import("std");
const expect = std.testing.expect;
fn somar(a: i32, b: i32) i32 {
return a + b;
}
fn dividir(a: f64, b: f64) !f64 {
if (b == 0) return error.DivisaoPorZero;
return a / b;
}
test "somar dois números positivos" {
try expect(somar(2, 3) == 5);
}
test "somar números negativos" {
try expect(somar(-1, -1) == -2);
}
test "somar com zero" {
try expect(somar(0, 42) == 42);
}
test "dividir números válidos" {
const resultado = try dividir(10, 2);
try expect(resultado == 5.0);
}
test "dividir por zero retorna erro" {
const resultado = dividir(10, 0);
try std.testing.expectError(error.DivisaoPorZero, resultado);
}
Executando Testes
# Executar todos os testes do projeto
zig build test
# Executar testes de um arquivo específico
zig test src/utils.zig
# Executar com filtro de nome
zig test src/main.zig --test-filter "somar"
# Executar com verbose
zig test src/main.zig --verbose
std.testing — API de Asserções
A biblioteca padrão oferece um conjunto rico de funções de asserção:
const testing = std.testing;
test "expect básico" {
try testing.expect(true);
try testing.expect(1 + 1 == 2);
}
test "expectEqual para comparação de valores" {
try testing.expectEqual(@as(i32, 42), somar(20, 22));
}
test "expectApproxEqAbs para floats" {
try testing.expectApproxEqAbs(@as(f64, 3.14159), calculaPi(), 0.001);
}
test "expectEqualStrings para strings" {
try testing.expectEqualStrings("olá", saudacao());
}
test "expectEqualSlices para slices" {
const esperado = [_]u8{ 1, 2, 3, 4, 5 };
const resultado = gerarSequencia();
try testing.expectEqualSlices(u8, &esperado, resultado);
}
test "expectFmt para formatação" {
try testing.expectFmt("42", "{}", .{@as(i32, 42)});
}
test "expectError para verificar erros" {
try testing.expectError(error.InvalidInput, processar("inválido"));
}
Alocador de Teste
O std.testing.allocator detecta memory leaks automaticamente:
test "sem memory leak" {
const allocator = std.testing.allocator;
var lista = std.ArrayList(u8).init(allocator);
defer lista.deinit(); // Se esquecer, o teste falha!
try lista.append(42);
try testing.expect(lista.items.len == 1);
}
test "detecta memory leak" {
const allocator = std.testing.allocator;
var lista = std.ArrayList(u8).init(allocator);
try lista.append(42);
// Esqueceu lista.deinit() — o teste falhará com erro de leak!
}
FailingAllocator para Testes de Resiliência
test "tratamento de falha de alocação" {
var failing = std.testing.FailingAllocator.init(
std.testing.allocator,
.{ .fail_index = 3 }, // Falha na 4ª alocação
);
const allocator = failing.allocator();
// Testar que o código trata corretamente falhas de alocação
const resultado = minhaFuncao(allocator);
try testing.expectError(error.OutOfMemory, resultado);
}
Testes de Integração
Separando Testes em Arquivos
Organize testes de integração em arquivos separados:
src/
├── main.zig
├── database.zig
├── http_handler.zig
tests/
├── test_database.zig
├── test_http.zig
└── test_integration.zig
// tests/test_integration.zig
const std = @import("std");
const app = @import("../src/main.zig");
test "fluxo completo de criação de usuário" {
const allocator = std.testing.allocator;
// Setup
var servidor = try app.Server.init(allocator, .{ .port = 0 });
defer servidor.deinit();
// Executar
const resposta = try servidor.handleRequest(.{
.method = .POST,
.path = "/usuarios",
.body = "{\"nome\": \"Maria\"}",
});
// Verificar
try std.testing.expectEqual(@as(u16, 201), resposta.status);
}
Configuração no build.zig
// Testes de integração no build.zig
const integration_tests = b.addTest(.{
.root_source_file = b.path("tests/test_integration.zig"),
.target = target,
.optimize = optimize,
});
const run_integration = b.addRunArtifact(integration_tests);
const integration_step = b.step("test-integration", "Executar testes de integração");
integration_step.dependOn(&run_integration.step);
Benchmarks
O Zig não inclui um framework de benchmark na stdlib, mas o ecossistema oferece opções maduras:
zig-bench
const bench = @import("zig-bench");
fn benchmarkSorting(timer: *bench.Timer) void {
var dados = gerarDadosAleatorios(10000);
timer.start();
std.sort.sort(u32, &dados, {}, std.sort.asc(u32));
timer.stop();
}
pub fn main() !void {
var b = bench.Benchmark.init(std.heap.page_allocator);
try b.add("sorting 10k elementos", benchmarkSorting);
try b.add("sorting 100k elementos", benchmarkSorting100k);
try b.run();
b.printResults();
}
Benchmark Manual com std.time
test "benchmark manual - parsing JSON" {
const iteracoes = 10000;
const json_input = @embedFile("test_data/grande.json");
const inicio = std.time.nanoTimestamp();
for (0..iteracoes) |_| {
const parsed = try std.json.parseFromSlice(
MeuTipo,
std.testing.allocator,
json_input,
.{},
);
defer parsed.deinit();
}
const fim = std.time.nanoTimestamp();
const duracao_ns = fim - inicio;
const por_iteracao = @divTrunc(duracao_ns, iteracoes);
std.debug.print("\nBenchmark: {} iterações em {}ms ({} ns/op)\n", .{
iteracoes,
@divTrunc(duracao_ns, 1_000_000),
por_iteracao,
});
}
Mocking e Test Doubles
Zig permite mocking elegante usando comptime:
fn HttpClient(comptime Impl: type) type {
return struct {
impl: Impl,
pub fn get(self: *@This(), url: []const u8) ![]const u8 {
return self.impl.get(url);
}
};
}
// Implementação real
const RealHttp = struct {
pub fn get(_: *@This(), url: []const u8) ![]const u8 {
_ = url;
// Requisição HTTP real
return "resposta real";
}
};
// Mock para testes
const MockHttp = struct {
resposta_mock: []const u8,
pub fn get(self: *@This(), _: []const u8) ![]const u8 {
return self.resposta_mock;
}
};
test "serviço com mock HTTP" {
var mock = MockHttp{ .resposta_mock = "{\"status\": \"ok\"}" };
var client = HttpClient(MockHttp){ .impl = mock };
_ = &client;
const resultado = try client.get("/api/health");
try std.testing.expectEqualStrings("{\"status\": \"ok\"}", resultado);
}
Boas Práticas
- Teste ao lado do código: Coloque
testblocks no mesmo arquivo da implementação - Use o testing.allocator: Detecte memory leaks automaticamente
- Nomeie testes descritivamente: Use nomes que descrevam o comportamento esperado
- Teste edge cases: Valores nulos, listas vazias, limites de overflow
- Automatize no CI: Execute
zig build testem cada push — veja nosso guia de ferramentas de debug
Próximos Passos
Explore as ferramentas de debug para depuração avançada, as ferramentas de profiling para otimização de performance, e as ferramentas de documentação para documentar seu código testado. Consulte nossos tutoriais e receitas para exemplos práticos.