Test Patterns em Zig: Mocks, Stubs, DI e Table-Driven Tests

Escrever testes e facil. Escrever bons testes que sejam manteneiros, rapidos e confiaveis e uma arte. Neste artigo, exploramos padroes avancados de teste em Zig, incluindo Arrange-Act-Assert, mocks usando interfaces de comptime, dependency injection e table-driven tests.

Continuacao do artigo sobre fundamentos de unit tests.

Arrange-Act-Assert (AAA)

O padrao AAA organiza cada teste em tres fases claras:

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

const ContaBancaria = struct {
    saldo: f64,
    titular: []const u8,

    pub fn depositar(self: *ContaBancaria, valor: f64) !void {
        if (valor <= 0) return error.ValorInvalido;
        self.saldo += valor;
    }

    pub fn sacar(self: *ContaBancaria, valor: f64) !void {
        if (valor <= 0) return error.ValorInvalido;
        if (valor > self.saldo) return error.SaldoInsuficiente;
        self.saldo -= valor;
    }
};

test "deposito aumenta saldo" {
    // Arrange
    var conta = ContaBancaria{ .saldo = 100.0, .titular = "Maria" };

    // Act
    try conta.depositar(50.0);

    // Assert
    try std.testing.expectApproxEqAbs(@as(f64, 150.0), conta.saldo, 0.01);
}

test "saque com saldo insuficiente retorna erro" {
    // Arrange
    var conta = ContaBancaria{ .saldo = 100.0, .titular = "Joao" };

    // Act + Assert
    try std.testing.expectError(error.SaldoInsuficiente, conta.sacar(200.0));
}

Dependency Injection para Testabilidade

Em vez de depender de implementacoes concretas, use interfaces via comptime:

const std = @import("std");

// Interface: qualquer tipo que implemente "lerTemperatura"
fn SensorCliente(comptime SensorType: type) type {
    return struct {
        sensor: SensorType,
        historico: std.ArrayList(f32),

        const Self = @This();

        pub fn init(sensor: SensorType, allocator: std.mem.Allocator) Self {
            return .{
                .sensor = sensor,
                .historico = std.ArrayList(f32).init(allocator),
            };
        }

        pub fn deinit(self: *Self) void {
            self.historico.deinit();
        }

        pub fn coletar(self: *Self) !f32 {
            const temp = try self.sensor.lerTemperatura();
            try self.historico.append(temp);
            return temp;
        }

        pub fn media(self: *const Self) f32 {
            if (self.historico.items.len == 0) return 0;
            var soma: f32 = 0;
            for (self.historico.items) |v| soma += v;
            return soma / @as(f32, @floatFromInt(self.historico.items.len));
        }
    };
}

// Implementacao real
const SensorReal = struct {
    pub fn lerTemperatura(_: *const SensorReal) !f32 {
        // Ler do hardware real
        return 23.5;
    }
};

// Mock para testes
const SensorMock = struct {
    valores: []const f32,
    idx: usize = 0,

    pub fn lerTemperatura(self: *const SensorMock) !f32 {
        if (self.idx >= self.valores.len) return error.SemDados;
        const v = self.valores[self.idx];
        // Nota: para mock mutavel, use *SensorMock
        return v;
    }
};

test "sensor cliente com mock" {
    const valores = [_]f32{ 20.0, 22.0, 24.0 };
    var mock = SensorMock{ .valores = &valores };
    var cliente = SensorCliente(SensorMock).init(mock, std.testing.allocator);
    defer cliente.deinit();

    const temp = try cliente.coletar();
    try std.testing.expectApproxEqAbs(@as(f32, 20.0), temp, 0.1);
}

Table-Driven Tests

Table-driven tests sao ideais para testar muitas combinacoes de entrada/saida:

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

fn classificarIMC(imc: f32) []const u8 {
    if (imc < 18.5) return "Abaixo do peso";
    if (imc < 25.0) return "Peso normal";
    if (imc < 30.0) return "Sobrepeso";
    return "Obeso";
}

test "classificacao de IMC" {
    const TestCase = struct {
        imc: f32,
        esperado: []const u8,
    };

    const casos = [_]TestCase{
        .{ .imc = 15.0, .esperado = "Abaixo do peso" },
        .{ .imc = 18.4, .esperado = "Abaixo do peso" },
        .{ .imc = 18.5, .esperado = "Peso normal" },
        .{ .imc = 22.0, .esperado = "Peso normal" },
        .{ .imc = 24.9, .esperado = "Peso normal" },
        .{ .imc = 25.0, .esperado = "Sobrepeso" },
        .{ .imc = 29.9, .esperado = "Sobrepeso" },
        .{ .imc = 30.0, .esperado = "Obeso" },
        .{ .imc = 40.0, .esperado = "Obeso" },
    };

    for (casos) |caso| {
        const resultado = classificarIMC(caso.imc);
        try std.testing.expectEqualStrings(caso.esperado, resultado);
    }
}

Mocks com Registro de Chamadas

Para verificar comportamento alem de retorno:

const std = @import("std");

const LoggerMock = struct {
    chamadas: std.ArrayList([]const u8),
    allocator: std.mem.Allocator,

    pub fn init(allocator: std.mem.Allocator) LoggerMock {
        return .{
            .chamadas = std.ArrayList([]const u8).init(allocator),
            .allocator = allocator,
        };
    }

    pub fn deinit(self: *LoggerMock) void {
        self.chamadas.deinit();
    }

    pub fn log(self: *LoggerMock, msg: []const u8) void {
        self.chamadas.append(msg) catch {};
    }

    // Verificacoes
    pub fn foiChamado(self: *const LoggerMock) bool {
        return self.chamadas.items.len > 0;
    }

    pub fn numeroChamadas(self: *const LoggerMock) usize {
        return self.chamadas.items.len;
    }

    pub fn ultimaChamada(self: *const LoggerMock) ?[]const u8 {
        if (self.chamadas.items.len == 0) return null;
        return self.chamadas.items[self.chamadas.items.len - 1];
    }
};

fn processarPedido(logger: *LoggerMock, pedido_id: u32) !void {
    logger.log("Processando pedido");
    // ... logica ...
    if (pedido_id == 0) return error.PedidoInvalido;
    logger.log("Pedido processado");
}

test "processarPedido loga corretamente" {
    var logger = LoggerMock.init(std.testing.allocator);
    defer logger.deinit();

    try processarPedido(&logger, 42);

    try std.testing.expect(logger.numeroChamadas() == 2);
    try std.testing.expectEqualStrings("Pedido processado", logger.ultimaChamada().?);
}

Test Fixtures

Para testes que compartilham setup complexo:

const std = @import("std");

const TestFixture = struct {
    allocator: std.mem.Allocator,
    dados: std.ArrayList(u8),
    temp_dir: ?[]const u8 = null,

    pub fn setup(allocator: std.mem.Allocator) !TestFixture {
        var fixture = TestFixture{
            .allocator = allocator,
            .dados = std.ArrayList(u8).init(allocator),
        };

        // Setup comum
        try fixture.dados.appendSlice("dados de teste");
        return fixture;
    }

    pub fn teardown(self: *TestFixture) void {
        self.dados.deinit();
        // Limpar temp files se necessario
    }
};

test "teste com fixture" {
    var fixture = try TestFixture.setup(std.testing.allocator);
    defer fixture.teardown();

    try std.testing.expect(fixture.dados.items.len > 0);
}

Exercicios

  1. Mock de HTTP client: Crie um mock que simule respostas HTTP e verifique que URLs corretas sao chamadas.

  2. Property-based testing: Implemente testes que verifiquem propriedades (ex: sort sempre retorna array ordenado, encode/decode sao inversos).

  3. Snapshot testing: Crie um sistema que salve a saida de funcoes em arquivos e compare com saidas anteriores.


Proximo Artigo

No proximo artigo, exploramos fuzz testing para encontrar bugs que testes unitarios nao capturam.

Conteudo Relacionado


Duvidas sobre padroes de teste? Participe da comunidade Zig Brasil!

Continue aprendendo Zig

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