Perguntas de Entrevista sobre Testing em Zig

Perguntas de Entrevista sobre Testing em Zig

Testes são fundamentais em qualquer base de código profissional. O Zig tem suporte a testes integrado diretamente na linguagem com blocos test, um allocator de teste que detecta memory leaks e integração com o build system. Este guia cobre perguntas práticas sobre testing em Zig para entrevistas.

Perguntas e Respostas

1. Como funciona o sistema de testes do Zig?

No Zig, testes são blocos test "nome" que vivem junto ao código-fonte. O compilador pode executá-los via zig test arquivo.zig. Os blocos test não são compilados em builds normais — apenas quando zig test é executado.

const std = @import("std");

fn somar(a: i32, b: i32) i32 {
    return a + b;
}

test "somar dois positivos" {
    try std.testing.expectEqual(@as(i32, 5), somar(2, 3));
}

test "somar com zero" {
    try std.testing.expectEqual(@as(i32, 7), somar(7, 0));
}

test "somar negativos" {
    try std.testing.expectEqual(@as(i32, -3), somar(-1, -2));
}

Cada bloco test é executado isoladamente. Se um try falha, o teste é reportado como falho com informação detalhada sobre o valor esperado vs. recebido.

2. Qual a diferença entre expect, expectEqual e expectEqualStrings?

const testing = std.testing;

test "expect — verifica booleano" {
    const x: i32 = 10;
    try testing.expect(x > 5);       // Verifica condição booleana
    try testing.expect(x != 0);
}

test "expectEqual — verifica igualdade de valores" {
    try testing.expectEqual(@as(i32, 42), 42);           // Inteiros
    try testing.expectEqual(@as(f64, 3.14), 3.14);       // Floats exatos
    try testing.expectEqual(@as(?i32, null), @as(?i32, null)); // Optionals
}

test "expectEqualStrings — compara conteúdo de strings" {
    const nome = "Zig";
    try testing.expectEqualStrings("Zig", nome);
    // Diferente de expectEqual, mostra diff detalhado em caso de falha
}

test "expectEqualSlices — compara slices elemento a elemento" {
    const esperado = [_]u8{ 1, 2, 3 };
    const obtido = [_]u8{ 1, 2, 3 };
    try testing.expectEqualSlices(u8, &esperado, &obtido);
}

test "expectApproxEqAbs — floats com tolerância" {
    try testing.expectApproxEqAbs(@as(f64, 3.14159), 3.14, 0.01);
}

test "expectError — verifica que erro específico é retornado" {
    const resultado: anyerror!i32 = error.Overflow;
    try testing.expectError(error.Overflow, resultado);
}

3. O que é o testing allocator e por que é importante?

O std.testing.allocator é um allocator especial que rastreia todas as alocações e, ao final do teste, verifica se toda memória foi liberada. Se houver leak, o teste falha automaticamente.

const std = @import("std");
const testing = std.testing;

fn criarLista(allocator: std.mem.Allocator) !std.ArrayList(i32) {
    var lista = std.ArrayList(i32).init(allocator);
    try lista.append(1);
    try lista.append(2);
    try lista.append(3);
    return lista;
}

test "lista sem memory leak" {
    var lista = try criarLista(testing.allocator);
    defer lista.deinit(); // Sem isso, o teste FALHA!

    try testing.expectEqual(@as(usize, 3), lista.items.len);
}

test "demonstração de leak detection" {
    // Se comentar o defer, o teste falha com:
    // "memory leak detected" + endereço e stack trace da alocação
    var buf = try testing.allocator.alloc(u8, 100);
    defer testing.allocator.free(buf);
    buf[0] = 42;
    try testing.expectEqual(@as(u8, 42), buf[0]);
}

4. Como organizar testes em projetos maiores?

// src/math.zig
pub fn fatorial(n: u64) u64 {
    if (n <= 1) return 1;
    var r: u64 = 1;
    for (2..n + 1) |i| r *= i;
    return r;
}

// Testes inline — vivem junto ao código
test "fatorial" {
    const testing = @import("std").testing;
    try testing.expectEqual(@as(u64, 1), fatorial(0));
    try testing.expectEqual(@as(u64, 120), fatorial(5));
}

// src/main.zig — referencia testes de submódulos
const math = @import("math.zig");

test {
    // Força o compilador a incluir testes de todos os imports
    @import("std").testing.refAllDecls(@This());
    _ = math; // Inclui testes de math.zig
}

No build.zig, configure testes com:

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);
const test_step = b.step("test", "Executa testes unitários");
test_step.dependOn(&run_unit_tests.step);

5. Como testar código que depende de I/O ou estado externo?

No Zig, a abordagem idiomática é injetar dependências via parâmetros (dependency injection):

const std = @import("std");

/// Processador que aceita qualquer Writer (injeção de dependência).
pub fn processar(dados: []const u8, writer: anytype) !usize {
    var contagem: usize = 0;
    for (dados) |c| {
        if (c >= 'A' and c <= 'Z') {
            try writer.writeByte(c + 32); // lowercase
            contagem += 1;
        } else {
            try writer.writeByte(c);
        }
    }
    return contagem;
}

test "processar converte maiúsculas" {
    var buf: [256]u8 = undefined;
    var stream = std.io.fixedBufferStream(&buf);
    const writer = stream.writer();

    const convertidos = try processar("Hello WORLD", writer);

    try std.testing.expectEqual(@as(usize, 7), convertidos);
    try std.testing.expectEqualStrings("hello world", stream.getWritten());
}

6. Como simular (mock) dependências em testes?

O Zig não tem um framework de mocking nativo, mas comptime generics permitem injeção limpa:

const std = @import("std");
const testing = std.testing;

/// Interface genérica via comptime — aceita implementação real ou mock.
fn HttpHandler(comptime Fetcher: type) type {
    return struct {
        pub fn obterStatus(fetcher: *Fetcher, url: []const u8) !u16 {
            const response = try fetcher.fetch(url);
            return response.status;
        }
    };
}

// Mock para testes
const MockFetcher = struct {
    chamadas: usize = 0,

    const Response = struct { status: u16 };

    pub fn fetch(self: *MockFetcher, url: []const u8) !Response {
        self.chamadas += 1;
        _ = url;
        return .{ .status = 200 };
    }
};

test "HttpHandler com mock" {
    var mock = MockFetcher{};
    const Handler = HttpHandler(MockFetcher);

    const status = try Handler.obterStatus(&mock, "https://example.com");

    try testing.expectEqual(@as(u16, 200), status);
    try testing.expectEqual(@as(usize, 1), mock.chamadas);
}

7. Como testar código concorrente em Zig?

const std = @import("std");
const testing = std.testing;

fn Contador() type {
    return struct {
        const Self = @This();
        valor: std.atomic.Value(u32),

        pub fn init() Self {
            return .{ .valor = std.atomic.Value(u32).init(0) };
        }

        pub fn incrementar(self: *Self) void {
            _ = self.valor.fetchAdd(1, .monotonic);
        }

        pub fn obter(self: *const Self) u32 {
            return self.valor.load(.monotonic);
        }
    };
}

test "contador atômico com múltiplas threads" {
    var contador = Contador().init();

    const num_threads = 4;
    const incrementos_por_thread = 1000;

    var threads: [num_threads]std.Thread = undefined;
    for (&threads) |*t| {
        t.* = try std.Thread.spawn(.{}, struct {
            fn func(c: *Contador()) void {
                for (0..incrementos_por_thread) |_| {
                    c.incrementar();
                }
            }
        }.func, .{&contador});
    }

    for (threads) |t| t.join();

    try testing.expectEqual(
        @as(u32, num_threads * incrementos_por_thread),
        contador.obter(),
    );
}

8. Como usar zig test com filtros e opções?

# Executar todos os testes
zig test src/main.zig

# Filtrar por nome do teste
zig test src/main.zig --test-filter "fatorial"

# Com sumário detalhado
zig test src/main.zig --summary all

# Sem captura de stderr (mostra std.debug.print)
zig test src/main.zig 2>&1

9. Como testar funções que podem causar panic?

No Zig, @panic não é catchable em código normal, mas em testes existe uma técnica usando std.testing.expectEqual com comportamento de runtime:

const std = @import("std");

fn dividir(a: i32, b: i32) i32 {
    if (b == 0) @panic("divisão por zero");
    return @divTrunc(a, b);
}

test "dividir funciona normalmente" {
    try std.testing.expectEqual(@as(i32, 5), dividir(10, 2));
    try std.testing.expectEqual(@as(i32, -3), dividir(-9, 3));
}

// Para testar panic, a abordagem idiomática é usar error unions
// em vez de @panic, para que erros sejam testáveis:
fn dividirSeguro(a: i32, b: i32) !i32 {
    if (b == 0) return error.DivisaoPorZero;
    return @divTrunc(a, b);
}

test "dividirSeguro retorna erro para zero" {
    try std.testing.expectError(error.DivisaoPorZero, dividirSeguro(10, 0));
}

10. Como escrever testes de propriedade (property-based testing)?

const std = @import("std");
const testing = std.testing;

fn inverterSlice(comptime T: type, slice: []T) void {
    if (slice.len <= 1) return;
    var i: usize = 0;
    var j = slice.len - 1;
    while (i < j) : ({
        i += 1;
        j -= 1;
    }) {
        const tmp = slice[i];
        slice[i] = slice[j];
        slice[j] = tmp;
    }
}

test "propriedade: inverter duas vezes volta ao original" {
    var prng = std.Random.DefaultPrng.init(42);
    const random = prng.random();

    // Testa com múltiplas entradas aleatórias
    for (0..100) |_| {
        const tamanho = random.intRangeAtMost(usize, 0, 50);
        var dados = try testing.allocator.alloc(u8, tamanho);
        defer testing.allocator.free(dados);

        // Preenche com dados aleatórios
        random.bytes(dados);

        // Salva cópia do original
        var original = try testing.allocator.alloc(u8, tamanho);
        defer testing.allocator.free(original);
        @memcpy(original, dados);

        // Inverte duas vezes
        inverterSlice(u8, dados);
        inverterSlice(u8, dados);

        // Propriedade: deve voltar ao original
        try testing.expectEqualSlices(u8, original, dados);
    }
}

test "propriedade: ordenar é idempotente" {
    var prng = std.Random.DefaultPrng.init(123);
    const random = prng.random();

    for (0..50) |_| {
        const tamanho = random.intRangeAtMost(usize, 1, 30);
        var dados = try testing.allocator.alloc(i32, tamanho);
        defer testing.allocator.free(dados);

        for (dados) |*d| d.* = random.int(i32);

        // Ordena
        std.mem.sort(i32, dados, {}, std.sort.asc(i32));

        // Propriedade: já está ordenado
        try testing.expect(std.sort.isSorted(i32, dados, {}, std.sort.asc(i32)));
    }
}

11. Como medir cobertura de testes em Zig?

O Zig possui suporte experimental a cobertura de código via LLVM:

# Compila com instrumentação de cobertura
zig test -fllvm --coverage src/main.zig

# Gera relatório (usando llvm-cov)
llvm-profdata merge default.profraw -o default.profdata
llvm-cov show ./zig-out/test -instr-profile=default.profdata

12. Quais as boas práticas de testing em Zig?

  1. Use testing.allocator para detectar leaks automaticamente
  2. Teste junto ao código — blocos test no mesmo arquivo
  3. Prefira error unions a @panic para código testável
  4. Injete dependências via parâmetros genéricos (comptime)
  5. Nomeie testes descritivamentetest "fatorial de zero retorna 1"
  6. Use refAllDecls no main para incluir testes de submódulos
  7. Teste edge cases — null, vazio, overflow, zero

Recursos Relacionados

Continue aprendendo Zig

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