Testes em Zig: Guia Completo de Zig Testing

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ísticaZigOutras Linguagens
Testes integradostest blocks no mesmo arquivoArquivos separados (geralmente)
Compilação condicionalbuiltin.is_test nativoFlags de compilação
PerformanceCódigo de teste compilado, não interpretadoInterpretado (Python, JS) ou VM (Java)
MocksComptime + structs flexíveisFrameworks complexos
Memory safetyDetecta vazamentos nos testesDepende da linguagem/runtime
Stack tracesDetalhados em falhas de testeVariável por linguagem

Vantagens Únicas do Zig Testing

  1. Zero overhead: Testes são compilados como código nativo — sem interpretação lenta
  2. Testes como documentação: Blocos test ficam junto ao código, servindo como exemplos vivos
  3. Memory leak detection: O test runner detecta automaticamente vazamentos de memória
  4. 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 try com testing.expect() — ela retorna um erro em caso de falha, e try propaga 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çãoUso
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 é um GeneralPurposeAllocator configurado 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

  1. Teste em múltiplas plataformas: Linux, macOS, Windows
  2. Teste com diferentes otimizações: Debug, ReleaseSafe, ReleaseFast
  3. Cross-compilation: Aproveite o poder de Zig para testar builds cruzados
  4. Formatação: Inclua zig fmt --check para manter consistência
  5. Cache: Use cache de zig-cache para 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

AspectoZigRustGoC++
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

  1. Use testing.allocator para detectar vazamentos automaticamente
  2. Teste os casos de erro — use expectError
  3. Mantenha testes rápidos — evite I/O lento em testes unitários
  4. Use nomes descritivos nos blocos test
  5. Mock dependências externas — não faça requisições HTTP reais nos testes
  6. 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.testing e suas funções de expect
  • Use testing.allocator para detectar vazamentos
  • Teste casos de sucesso E casos de erro
  • Use mocks para isolar unidades
  • Organize testes inline ou em arquivos dedicados
  • Configure build.zig para 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:


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

Continue aprendendo Zig

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