Uma das decisões de design mais acertadas da linguagem Zig foi incorporar testes como cidadãos de primeira classe. Não existe framework externo, não existe configuração especial — testes vivem no mesmo arquivo que o código que testam e são executados com um único comando. Essa filosofia elimina a fricção que, em outras linguagens, faz desenvolvedores pularem testes.
Neste guia completo, vamos cobrir desde o básico até padrões avançados como fuzz testing e integração com CI/CD.
Seu Primeiro Teste em Zig
Em Zig, testes são declarados com a palavra-chave test seguida de uma string descritiva:
const std = @import("std");
const expect = std.testing.expect;
fn somar(a: i32, b: i32) i32 {
return a + b;
}
test "somar dois números positivos" {
const resultado = somar(2, 3);
try expect(resultado == 5);
}
test "somar com zero" {
try expect(somar(0, 42) == 42);
try expect(somar(42, 0) == 42);
}
Execute com:
zig test arquivo.zig
Se todos os testes passarem, a saída será:
All 2 tests passed.
Simples assim. Sem main(), sem imports de frameworks, sem boilerplate.
Funções de Asserção
O módulo std.testing oferece várias funções de asserção:
expect e expectEqual
const testing = std.testing;
test "asserções básicas" {
// Condição booleana
try testing.expect(true);
// Igualdade com mensagem de erro informativa
try testing.expectEqual(@as(i32, 42), somar(40, 2));
// Igualdade aproximada para floats
try testing.expectApproxEqAbs(@as(f64, 3.14), 3.14159, 0.01);
// Comparação de strings
try testing.expectEqualStrings("hello", "hello");
// Comparação de slices
try testing.expectEqualSlices(u8, &[_]u8{ 1, 2, 3 }, &[_]u8{ 1, 2, 3 });
}
expectError: Testando Error Unions
Zig tem um sistema de error handling poderoso, e testá-lo é igualmente expressivo:
const DivisionError = error{DivisionByZero};
fn dividir(a: f64, b: f64) DivisionError!f64 {
if (b == 0.0) return error.DivisionByZero;
return a / b;
}
test "divisão normal" {
const resultado = try dividir(10.0, 2.0);
try testing.expectEqual(@as(f64, 5.0), resultado);
}
test "divisão por zero retorna erro" {
const resultado = dividir(10.0, 0.0);
try testing.expectError(error.DivisionByZero, resultado);
}
Detecção de Memory Leaks com Testing Allocator
Este é um dos recursos mais poderosos de testes em Zig. O std.testing.allocator detecta automaticamente memory leaks — se qualquer memória alocada não for liberada ao final do teste, o teste falha:
test "sem memory leaks" {
const allocator = std.testing.allocator;
var lista = std.ArrayList(u8).init(allocator);
defer lista.deinit(); // Se esquecer isso, o teste falha!
try lista.append(42);
try lista.append(100);
try testing.expectEqual(@as(usize, 2), lista.items.len);
}
test "detecta leak intencional" {
const allocator = std.testing.allocator;
// Isso FALHARÁ — memória alocada mas nunca liberada
const ptr = try allocator.alloc(u8, 100);
_ = ptr;
// Esqueceu: allocator.free(ptr);
}
Para quem vem de linguagens com garbage collector, isso pode parecer tedioso. Mas na prática, o allocator de testes encontra bugs de memória que em C levariam horas com Valgrind. Veja nosso artigo sobre estratégias de alocação de memória para aprofundar.
Testes Table-Driven
O padrão de table-driven tests, popular em Go, é igualmente elegante em Zig:
test "fibonacci - table driven" {
const TestCase = struct {
input: u32,
expected: u32,
};
const cases = [_]TestCase{
.{ .input = 0, .expected = 0 },
.{ .input = 1, .expected = 1 },
.{ .input = 2, .expected = 1 },
.{ .input = 5, .expected = 5 },
.{ .input = 10, .expected = 55 },
.{ .input = 20, .expected = 6765 },
};
for (cases) |tc| {
const resultado = fibonacci(tc.input);
try testing.expectEqual(tc.expected, resultado);
}
}
Esse padrão é excelente para testar funções puras com múltiplas entradas. Se você conhece table-driven tests do Go, vai se sentir em casa — e pode comparar as abordagens no Go Lang Brasil.
Organização de Testes
Testes no Mesmo Arquivo
A convenção em Zig é colocar testes no mesmo arquivo que o código:
// math.zig
pub fn fatorial(n: u32) u32 {
if (n <= 1) return 1;
return n * fatorial(n - 1);
}
// Testes ficam no final do arquivo
test "fatorial de 0" {
try testing.expectEqual(@as(u32, 1), fatorial(0));
}
test "fatorial de 5" {
try testing.expectEqual(@as(u32, 120), fatorial(5));
}
Executando Testes Específicos
Use --test-filter para executar apenas testes que contenham uma substring no nome:
# Apenas testes com "fatorial" no nome
zig test math.zig --test-filter "fatorial"
# Testes de um módulo específico via build system
zig build test --test-filter "divisão"
Testes com Build System
Para projetos maiores, configure testes no build.zig:
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
// Executável principal
const exe = b.addExecutable(.{
.name = "meu-projeto",
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
// Testes unitários
const unit_tests = b.addTest(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
const run_tests = b.addRunArtifact(unit_tests);
const test_step = b.step("test", "Executar testes unitários");
test_step.dependOn(&run_tests.step);
b.installArtifact(exe);
}
Depois basta rodar:
zig build test
Testando Código Comptime
Uma particularidade de Zig: você pode testar código que roda em compile-time:
fn comptimeFibonacci(comptime n: u32) u32 {
if (n <= 1) return n;
return comptimeFibonacci(n - 1) + comptimeFibonacci(n - 2);
}
test "fibonacci em comptime" {
// Este cálculo é feito inteiramente em tempo de compilação
comptime {
const result = comptimeFibonacci(10);
if (result != 55) @compileError("fibonacci(10) deveria ser 55");
}
// Também funciona com expect normal
try testing.expectEqual(@as(u32, 55), comptime comptimeFibonacci(10));
}
Para mais sobre comptime, veja nosso artigo dedicado: Comptime em Zig: Metaprogramação sem Macros.
Mocking e Injeção de Dependência
Zig não tem um framework de mocking embutido, mas o design da linguagem facilita injeção de dependência via parâmetros de tipo e interfaces:
// Interface via 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);
}
};
}
// Mock para testes
const MockHttp = struct {
response: []const u8,
pub fn get(self: *MockHttp, _: []const u8) ![]const u8 {
return self.response;
}
};
test "fetch com mock" {
var mock = MockHttp{ .response = "{\"status\": \"ok\"}" };
var client = HttpClient(MockHttp){ .impl = mock };
const response = try client.get("https://api.example.com/data");
try testing.expectEqualStrings("{\"status\": \"ok\"}", response);
}
Esse padrão de polimorfismo em compile-time é uma das forças de Zig — sem vtables, sem overhead de runtime.
Fuzz Testing
A partir do Zig 0.12+, fuzz testing está disponível nativamente:
test "fuzz parser de número" {
// O fuzzer gera inputs automaticamente
try std.testing.fuzz(.{}, struct {
fn testOne(input: []const u8) !void {
// Tenta parsear como número
_ = std.fmt.parseInt(i64, input, 10) catch return;
// Se parseou, verifica que é válido
}
}.testOne);
}
O fuzzer gera milhares de inputs aleatórios buscando crashes, panics ou erros inesperados. É especialmente valioso para testar parsers, serializers e código que processa dados externos.
Integração com CI/CD
Para rodar testes em pipelines de CI/CD, o comando é direto:
# Exemplo com Gitea Actions
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Zig
uses: goto-bus-stop/setup-zig@v2
with:
version: 0.13.0
- name: Run tests
run: zig build test
- name: Run tests with release optimization
run: zig build test -Doptimize=ReleaseSafe
Uma boa prática é rodar testes tanto em modo Debug (com todas as safety checks) quanto em ReleaseSafe (para encontrar bugs que só aparecem com otimizações).
Coverage com kcov
O Zig gera binários de teste compatíveis com ferramentas de coverage como kcov:
# Compilar testes como executável
zig test src/main.zig --test-cmd kcov --test-cmd ./coverage --test-cmd-bin
# O relatório HTML estará em ./coverage/
Isso permite integrar relatórios de cobertura de código em seu pipeline de CI.
Boas Práticas para Testes em Zig
- Testes perto do código: mantenha testes no mesmo arquivo — reduz contexto necessário
- Use
testing.allocatorsempre: detecta leaks automaticamente - Testes devem ser rápidos: evite I/O real; use mocks para network e filesystem
- Testes determinísticos: nunca dependa de tempo real ou dados aleatórios (exceto fuzz)
- Nomes descritivos:
test "multiplicação de matriz 3x3 com identidade">test "test1" - Table-driven para múltiplas entradas: evita duplicação de lógica de teste
- Teste error paths: use
expectError— o caminho de erro é tão importante quanto o feliz - Rode em múltiplos modos: Debug para safety, ReleaseSafe para otimização
Para validar código com SIMD e processamento vetorial, combine estas práticas com os exemplos do nosso artigo sobre SIMD em Zig.
Comparação com Testes em Outras Linguagens
| Feature | Zig | Rust | Go | Python |
|---|---|---|---|---|
| Testes built-in | ✅ test blocks | ✅ #[test] | ✅ func Test* | ❌ unittest/pytest |
| Detecção de leaks | ✅ testing.allocator | ❌ (precisa Miri) | N/A (GC) | N/A (GC) |
| Fuzz built-in | ✅ std.testing.fuzz | ❌ (cargo-fuzz) | ✅ testing.F | ❌ (hypothesis) |
| Coverage | Via kcov | Via tarpaulin | Via go test -cover | Via coverage.py |
| Tempo de compilação | Rápido | Lento | Rápido | N/A |
Se você vem do ecossistema Rust e quer comparar abordagens de teste, veja o Rust Lang Brasil. Para a abordagem de testing do Go, confira o Go Lang Brasil. E para quem quer comparar com pytest, visite o Python Dev Brasil.
Conclusão
O sistema de testes de Zig demonstra a filosofia da linguagem: simplicidade sem sacrificar poder. Com blocos test integrados, testing.allocator para detecção de leaks e fuzz testing nativo, você tem tudo que precisa para escrever software confiável sem dependências externas.
Comece testando funções simples com expect, evolua para table-driven tests, e depois explore fuzz testing para inputs não confiáveis. E para consultas rápidas sobre a sintaxe de testes, mantenha nosso cheatsheet de testing à mão.
Para se aprofundar em testes de propriedade em Zig, veja nosso artigo sobre testes de propriedade, e explore os frameworks de testing do ecossistema para necessidades mais avançadas.