Testes são fundamentais para garantir a qualidade e confiabilidade do seu código. Zig não apenas facilita a escrita de testes — ela os torna uma parte natural e integrada do workflow de desenvolvimento. Neste guia completo, você vai aprender tudo sobre testes em Zig, desde o básico até técnicas avançadas de mocking e integração com CI/CD.
Por que Testar em Zig?
Antes de mergulharmos na prática, vamos entender por que Zig se destaca quando o assunto é testing:
| Característica | Zig | Outras Linguagens |
|---|---|---|
| Testes integrados | test blocks no mesmo arquivo | Arquivos separados (geralmente) |
| Compilação condicional | builtin.is_test nativo | Flags de compilação |
| Performance | Código de teste compilado, não interpretado | Interpretado (Python, JS) ou VM (Java) |
| Mocks | Comptime + structs flexíveis | Frameworks complexos |
| Memory safety | Detecta vazamentos nos testes | Depende da linguagem/runtime |
| Stack traces | Detalhados em falhas de teste | Variável por linguagem |
Vantagens Únicas do Zig Testing
- Zero overhead: Testes são compilados como código nativo — sem interpretação lenta
- Testes como documentação: Blocos
testficam junto ao código, servindo como exemplos vivos - Memory leak detection: O test runner detecta automaticamente vazamentos de memória
- Determinístico: Sem garbage collector causando comportamentos não-determinísticos
O Módulo std.testing
A biblioteca padrão de Zig inclui std.testing, que fornece tudo o que você precisa para escrever testes eficazes. Vamos conhecer as principais ferramentas:
Importando e Estrutura Básica
const std = @import("std");
const testing = std.testing;
test "descrição do teste" {
// Seu código de teste aqui
try testing.expect(true);
}
O bloco test é uma construção de primeiro nível em Zig — não está dentro de funções, mas sim no nível do arquivo, ao lado de funções, structs e outras declarações.
Asserts Básicos: O Coração dos Testes
O std.testing fornece várias funções de assertion para diferentes cenários:
expect: O Assert Fundamental
const std = @import("std");
const testing = std.testing;
test "expect básico" {
const resultado = soma(2, 3);
try testing.expect(resultado == 5);
}
fn soma(a: i32, b: i32) i32 {
return a + b;
}
Dica: Sempre use
trycomtesting.expect()— ela retorna um erro em caso de falha, etrypropaga isso corretamente.
expectEqual: Comparação de Igualdade
Para valores que você quer comparar diretamente:
test "expectEqual" {
const esperado: i32 = 42;
const atual = calculaResposta();
try testing.expectEqual(esperado, atual);
}
fn calculaResposta() i32 {
return 42;
}
expectEqual mostra uma mensagem de erro clara indicando qual valor era esperado e qual foi obtido.
expectEqualStrings: Para Strings
test "comparação de strings" {
const nome = "ZigLang";
try testing.expectEqualStrings("ZigLang", nome);
}
expectError: Testando Erros
Quando você quer garantir que uma função retorna um erro específico:
const ErroDivisao = error{DivisaoPorZero};
fn divide(a: i32, b: i32) !i32 {
if (b == 0) return ErroDivisao.DivisaoPorZero;
return @divTrunc(a, b);
}
test "expectError" {
// Testa que a função RETORNA o erro esperado
try testing.expectError(ErroDivisao.DivisaoPorZero, divide(10, 0));
// Testa que a função NÃO retorna erro
const resultado = try divide(10, 2);
try testing.expectEqual(5, resultado);
}
Outros Asserts Úteis
| Função | Uso |
|---|---|
expect(value) | Verifica se booleano é true |
expectEqual(expected, actual) | Compara dois valores |
expectEqualStrings(expected, actual) | Compara strings |
expectEqualSlices(T, expected, actual) | Compara slices |
expectError(expected_error, actual_error_union) | Verifica erro específico |
expectApproxEqAbs(expected, actual, tolerance) | Floats com tolerância absoluta |
expectApproxEqRel(expected, actual, tolerance) | Floats com tolerância relativa |
Testes Unitários: A Base da Qualidade
Testes unitários verificam funções individuais em isolamento. Em Zig, eles ficam no mesmo arquivo da implementação — uma prática que melhora a manutenibilidade.
Padrão de Organização
// math.zig
const std = @import("std");
const testing = std.testing;
// ============ IMPLEMENTAÇÃO ============
pub fn fatorial(n: u32) u64 {
if (n <= 1) return 1;
return n * fatorial(n - 1);
}
pub fn ehPrimo(n: u32) bool {
if (n < 2) return false;
if (n == 2) return true;
if (n % 2 == 0) return false;
var i: u32 = 3;
while (i * i <= n) : (i += 2) {
if (n % i == 0) return false;
}
return true;
}
// ============ TESTES ============
test "fatorial de números pequenos" {
try testing.expectEqual(1, fatorial(0));
try testing.expectEqual(1, fatorial(1));
try testing.expectEqual(2, fatorial(2));
try testing.expectEqual(6, fatorial(3));
try testing.expectEqual(24, fatorial(4));
}
test "fatorial de número maior" {
try testing.expectEqual(120, fatorial(5));
try testing.expectEqual(3628800, fatorial(10));
}
test "verificação de primos" {
try testing.expect(!ehPrimo(0));
try testing.expect(!ehPrimo(1));
try testing.expect(ehPrimo(2));
try testing.expect(ehPrimo(3));
try testing.expect(!ehPrimo(4));
try testing.expect(ehPrimo(17));
try testing.expect(!ehPrimo(18));
}
Executando Testes
# Todos os testes do arquivo
zig test math.zig
# Testes de um pacote inteiro
zig build test
# Testes com mais detalhes
zig test math.zig --test-cmd echo --test-cmd-bin
Testando com Allocators
Um dos superpoderes de Zig é o controle explícito de memória. Nos testes, você deve usar testing.allocator — ele automaticamente detecta vazamentos:
const std = @import("std");
const testing = std.testing;
fn duplicaString(allocator: std.mem.Allocator, s: []const u8) ![]u8 {
const resultado = try allocator.alloc(u8, s.len);
@memcpy(resultado, s);
return resultado;
}
test "duplicaString não vaza memória" {
const allocator = testing.allocator;
const original = "Hello, Zig!";
const copia = try duplicaString(allocator, original);
defer allocator.free(copia); // Importante: libera a memória
try testing.expectEqualStrings(original, copia);
}
// Este teste FALHARIA se esquecêssemos o defer free:
test "detecta vazamento de memória" {
const allocator = testing.allocator;
const copia = try duplicaString(allocator, "teste");
_ = copia;
// Esquecemos o defer allocator.free(copia)!
// O teste falhará reportando memory leak
}
Importante:
testing.allocatoré umGeneralPurposeAllocatorconfigurado para detectar vazamentos. Se você esquecer de liberar memória, o teste falha automaticamente.
Testes de Integração
Testes de integração verificam múltiplas partes do sistema trabalhando juntas. Em Zig, eles geralmente ficam em um diretório tests/.
Estrutura de Projeto
meu-projeto/
├── build.zig
├── src/
│ ├── main.zig
│ └── database.zig
└── tests/
└── integration_tests.zig
Exemplo de Teste de Integração
// tests/integration_tests.zig
const std = @import("std");
const testing = std.testing;
// Importa os módulos que queremos testar
const database = @import("../src/database.zig");
const TestContext = struct {
db: database.Database,
allocator: std.mem.Allocator,
fn init(allocator: std.mem.Allocator) !TestContext {
var ctx: TestContext = undefined;
ctx.allocator = allocator;
ctx.db = try database.Database.init(allocator, ":memory:");
return ctx;
}
fn deinit(self: *TestContext) void {
self.db.deinit();
}
};
test "ciclo completo de usuário" {
var ctx = try TestContext.init(testing.allocator);
defer ctx.deinit();
// Cria usuário
const user_id = try ctx.db.createUser(.{
.name = "João Silva",
.email = "joao@exemplo.com",
});
try testing.expect(user_id > 0);
// Busca usuário
const user = try ctx.db.getUser(user_id);
try testing.expectEqualStrings("João Silva", user.name);
try testing.expectEqualStrings("joao@exemplo.com", user.email);
// Atualiza usuário
try ctx.db.updateUser(user_id, .{ .name = "João S." });
const updated = try ctx.db.getUser(user_id);
try testing.expectEqualStrings("João S.", updated.name);
// Deleta usuário
try ctx.db.deleteUser(user_id);
try testing.expectError(error.UserNotFound, ctx.db.getUser(user_id));
}
Mocking em Zig: Simulação Elegante
Mocking (simulação de dependências) é essencial para testar código em isolamento. Zig torna isso elegante usando comptime e interfaces implícitas.
Padrão de Mock com Comptime
// http_client.zig
const std = @import("std");
// Define uma interface genérica para HTTP
pub fn HttpClient(comptime T: type) type {
return struct {
impl: T,
pub fn get(self: @This(), url: []const u8) ![]u8 {
return self.impl.get(url);
}
};
}
// Implementação real
pub const RealHttpClient = struct {
allocator: std.mem.Allocator,
pub fn get(self: RealHttpClient, url: []const u8) ![]u8 {
// Faz requisição HTTP real
_ = self;
_ = url;
return "{"status":"ok"}";
}
};
// Implementação mock para testes
pub const MockHttpClient = struct {
responses: std.StringHashMap([]const u8),
pub fn init(allocator: std.mem.Allocator) MockHttpClient {
return .{
.responses = std.StringHashMap([]const u8).init(allocator),
};
}
pub fn deinit(self: *MockHttpClient) void {
self.responses.deinit();
}
pub fn addResponse(self: *MockHttpClient, url: []const u8, response: []const u8) !void {
try self.responses.put(url, response);
}
pub fn get(self: MockHttpClient, url: []const u8) ![]u8 {
return self.responses.get(url) orelse error.NotFound;
}
};
Usando o Mock nos Testes
// weather_service.zig
const std = @import("std");
const http = @import("http_client.zig");
pub fn WeatherService(comptime Client: type) type {
return struct {
client: http.HttpClient(Client),
pub fn getTemperature(self: @This(), city: []const u8) !f32 {
const url = try std.fmt.allocPrint(
self.client.impl.allocator,
"https://api.weather.com/{s}",
.{city}
);
defer self.client.impl.allocator.free(url);
const response = try self.client.get(url);
// Parse JSON e extrai temperatura...
_ = response;
return 25.5;
}
};
}
// ============ TESTES ============
const testing = std.testing;
test "WeatherService com mock" {
var mock = http.MockHttpClient.init(testing.allocator);
defer mock.deinit();
try mock.addResponse(
"https://api.weather.com/SaoPaulo",
"{\"temp\": 28.5}"
);
const client = http.HttpClient(http.MockHttpClient){ .impl = mock };
const service = WeatherService(http.MockHttpClient){ .client = client };
const temp = try service.getTemperature("SaoPaulo");
try testing.expectApproxEqAbs(25.5, temp, 0.01);
}
Organização de Testes em Projetos Maiores
À medida que seu projeto cresce, você precisa de uma estratégia de organização:
Estratégia 1: Testes Inline (Pequenos/Médios)
Mantenha testes no mesmo arquivo da implementação para contexto máximo:
// src/parser.zig
pub fn Parser(comptime T: type) type {
return struct {
// implementação...
};
}
// Testes inline - ideal para unidade
test "Parser.parseNumber" {
const p = Parser(i32){};
try testing.expectEqual(42, try p.parseNumber("42"));
}
test "Parser.parseNumber erro" {
const p = Parser(i32){};
try testing.expectError(error.InvalidNumber, p.parseNumber("abc"));
}
Estratégia 2: Arquivo de Testes Dedicado (Médios/Grandes)
Para módulos complexos, crie parser_test.zig:
// src/parser_test.zig
const std = @import("std");
const testing = std.testing;
const parser = @import("parser.zig");
// Testes extensos de parser...
Estrutura Recomendada
project/
├── build.zig
├── src/
│ ├── main.zig
│ ├── parser.zig # com testes inline simples
│ ├── database.zig # com testes inline
│ └── utils.zig # com testes inline
├── tests/
│ ├── integration/
│ │ ├── api_tests.zig # testes de API
│ │ └── database_tests.zig
│ └── e2e/
│ └── user_flow_tests.zig
└── test_runner.zig # configuração customizada (opcional)
Testes e build.zig
O sistema de build de Zig facilita a execução de testes em diferentes configurações:
Configuração Básica
// build.zig
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
// Compila o executável principal
const exe = b.addExecutable(.{
.name = "meu-app",
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
b.installArtifact(exe);
// ============ TESTES ============
// Testes unitários
const unit_tests = b.addTest(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
const run_unit_tests = b.addRunArtifact(unit_tests);
// Testes de integração
const integration_tests = b.addTest(.{
.root_source_file = b.path("tests/integration_tests.zig"),
.target = target,
.optimize = optimize,
});
const run_integration_tests = b.addRunArtifact(integration_tests);
// Step "test" roda ambos
const test_step = b.step("test", "Run all tests");
test_step.dependOn(&run_unit_tests.step);
test_step.dependOn(&run_integration_tests.step);
// Step "test-unit" só para unitários
const test_unit_step = b.step("test-unit", "Run unit tests only");
test_unit_step.dependOn(&run_unit_tests.step);
// Step "test-integration" só para integração
const test_integration_step = b.step("test-integration", "Run integration tests only");
test_integration_step.dependOn(&run_integration_tests.step);
}
Executando Testes via Build
# Todos os testes
zig build test
# Só unitários
zig build test-unit
# Só integração
zig build test-integration
# Com otimização ReleaseFast (para testar performance)
zig build test -Doptimize=ReleaseFast
# Para um target específico
zig build test -Dtarget=aarch64-linux-gnu
CI/CD: Integração Contínua
Automatizar testes em CI/CD garante que seu código sempre funcione. Aqui estão configurações para as principais plataformas:
GitHub Actions
# .github/workflows/test.yml
name: Tests
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
zig-version: [0.13.0, master]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- name: Setup Zig
uses: goto-bus-stop/setup-zig@v2
with:
version: ${{ matrix.zig-version }}
- name: Cache Zig dependencies
uses: actions/cache@v3
with:
path: zig-cache
key: ${{ runner.os }}-zig-${{ hashFiles('**/build.zig.zon') }}
- name: Run tests
run: zig build test --summary all
- name: Run tests (ReleaseSafe)
run: zig build test -Doptimize=ReleaseSafe
- name: Check formatting
run: zig fmt --check src/
GitLab CI
# .gitlab-ci.yml
stages:
- test
- build
variables:
ZIG_VERSION: "0.13.0"
test:zig:
stage: test
image: alpine:latest
before_script:
- apk add --no-cache curl tar
- curl -L https://ziglang.org/download/${ZIG_VERSION}/zig-linux-x86_64-${ZIG_VERSION}.tar.xz | tar xJ
- export PATH=$PWD/zig-linux-x86_64-${ZIG_VERSION}:$PATH
script:
- zig build test
- zig fmt --check src/
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- zig-cache/
test:cross-compile:
stage: test
image: alpine:latest
before_script:
- apk add --no-cache curl tar
- curl -L https://ziglang.org/download/${ZIG_VERSION}/zig-linux-x86_64-${ZIG_VERSION}.tar.xz | tar xJ
- export PATH=$PWD/zig-linux-x86_64-${ZIG_VERSION}:$PATH
script:
# Testa cross-compilation para diferentes targets
- zig build -Dtarget=aarch64-linux-gnu
- zig build -Dtarget=x86_64-windows-gnu
- zig build -Dtarget=wasm32-freestanding
Dicas para CI/CD
- Teste em múltiplas plataformas: Linux, macOS, Windows
- Teste com diferentes otimizações: Debug, ReleaseSafe, ReleaseFast
- Cross-compilation: Aproveite o poder de Zig para testar builds cruzados
- Formatação: Inclua
zig fmt --checkpara manter consistência - Cache: Use cache de
zig-cachepara acelerar builds
Técnicas Avançadas de Teste
Testando Código Assíncrono
test "async operation" {
const frame = async fetchData();
const result = await frame;
try testing.expectEqual(200, result.status);
}
Testes Parametrizados com Comptime
test "parametrized tests" {
const test_cases = .{
.{ .input = 0, .expected = 1 },
.{ .input = 1, .expected = 1 },
.{ .input = 5, .expected = 120 },
};
inline for (test_cases) |tc| {
const result = fatorial(tc.input);
try testing.expectEqual(tc.expected, result);
}
}
Testes com Side Effects Controlados
var test_counter: u32 = 0;
test "conta execuções" {
test_counter += 1;
try testing.expect(test_counter > 0);
}
Cobertura de Código
Zig ainda não tem ferramenta nativa de cobertura, mas você pode usar kcov ou llvm-cov:
# Compila com cobertura
zig test src/main.zig -femit-llvm-ir
# Ou use kcov
kcov /tmp/coverage zig test src/main.zig
Comparação: Zig vs Outras Linguagens
| Aspecto | Zig | Rust | Go | C++ |
|---|---|---|---|---|
| Testes inline | ✅ Sim | ⚠️ Atributos | ✅ Sim | ❌ Arquivos separados |
| Memory leak detection | ✅ Automático | ✅ LeakSanitizer | ⚠️ Runtime | ❌ Manual |
| Test runner built-in | ✅ Sim | ✅ Sim | ✅ Sim | ❌ Não (precisa framework) |
| Mocking | ✅ Comptime | ⚠️ Macros/libs | ✅ Interfaces | ⚠️ Frameworks |
| Compile-time tests | ✅ Comptime | ⚠️ Const eval | ❌ Não | ⚠️ Templates |
| Cross-test (CI) | ✅ Nativo | ⚠️ Cargo cross | ⚠️ GOOS/GOARCH | ❌ Complexo |
Boas Práticas e Anti-Padrões
✅ Faça
- Use
testing.allocatorpara detectar vazamentos automaticamente - Teste os casos de erro — use
expectError - Mantenha testes rápidos — evite I/O lento em testes unitários
- Use nomes descritivos nos blocos
test - Mock dependências externas — não faça requisições HTTP reais nos testes
- Teste edge cases — zeros, vazios, máximos, mínimos
❌ Não Faça
// RUIM: Teste sem descrição clara
test "test1" { ... }
// RUIM: Não usar testing.allocator
const allocator = std.heap.page_allocator;
// RUIM: Teste que depende de estado externo
test "usar arquivo real" {
const file = try std.fs.cwd().openFile("/tmp/data.txt", .{});
// Pode falhar em CI!
}
// RUIM: Não testar erros
test "só sucesso" {
const result = divide(10, 2); // Esqueceu de testar divisão por zero!
}
Padrões Comuns de Teste
Arrange-Act-Assert (AAA)
test "segue padrão AAA" {
// Arrange (Preparar)
const allocator = testing.allocator;
const numerador: i32 = 10;
const denominador: i32 = 2;
// Act (Agir)
const resultado = divide(numerador, denominador);
// Assert (Verificar)
try testing.expectEqual(5, resultado);
}
Given-When-Then
test "given user is authenticated when accessing profile then returns data" {
// Given
const auth = MockAuth{ .authenticated = true };
const service = UserService.init(auth);
// When
const profile = try service.getProfile(123);
// Then
try testing.expectEqualStrings("João", profile.name);
}
Resumo: Checklist de Testes em Zig
- Use
std.testinge suas funções de expect - Use
testing.allocatorpara detectar vazamentos - Teste casos de sucesso E casos de erro
- Use mocks para isolar unidades
- Organize testes inline ou em arquivos dedicados
- Configure
build.zigpara múltiplos tipos de teste - Integre com CI/CD para testes automatizados
- Teste cross-compilation se relevante
- Mantenha testes rápidos e determinísticos
Próximos Passos
Agora que você domina testes em Zig, continue seu aprendizado:
- 📖 Gerenciamento de Memória em Zig: Allocators Explicados — Entenda como
testing.allocatorfunciona - 🔧 Zig Build System: Guia Completo do build.zig — Aprofunde-se em configurações avançadas de build
- 🐛 Tratamento de Erros em Zig: Guia Completo — Domine error unions para testes mais robustos
- 🚀 Comptime em Zig: O Poder da Execução em Tempo de Compilação — Crie mocks elegantes com metaprogramação
Gostou deste tutorial? Compartilhe com outros desenvolvedores e ajude a comunidade Zig Brasil a crescer! Encontrou algum erro ou tem sugestões? Entre em contato ou abra uma issue no nosso repositório.
Última atualização: 09 de fevereiro de 2026
Versão do Zig: 0.13.0