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
Mock de HTTP client: Crie um mock que simule respostas HTTP e verifique que URLs corretas sao chamadas.
Property-based testing: Implemente testes que verifiquem propriedades (ex: sort sempre retorna array ordenado, encode/decode sao inversos).
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
- Artigo anterior: Unit Tests Fundamentos
- Zig Design Patterns — Padroes de projeto
- Property Testing em Zig — Testes baseados em propriedades
Duvidas sobre padroes de teste? Participe da comunidade Zig Brasil!