Testes unitarios validam funcoes isoladas. Testes de integracao validam que componentes funcionam juntos corretamente — com o file system, rede, banco de dados e subprocessos reais. Zig oferece ferramentas poderosas para escrever testes de integracao confiaveis sem frameworks externos.
Continuacao do artigo sobre fuzz testing em Zig.
Unit Tests vs Integration Tests
| Aspecto | Unit Tests | Integration Tests |
|---|---|---|
| Escopo | Funcao/modulo individual | Multiplos componentes |
| Dependencias | Mockadas | Reais |
| Velocidade | Milissegundos | Segundos |
| Isolamento | Total | Parcial |
| Foco | Logica de negocio | Interacao entre partes |
Testando File System
Diretorio Temporario para Testes
O Zig fornece std.testing.tmpDir() para criar diretorios temporarios que sao limpos automaticamente:
const std = @import("std");
const fs = std.fs;
const expect = std.testing.expect;
/// Sistema de configuracao que le/escreve arquivos
const ConfigManager = struct {
dir: fs.Dir,
const Config = struct {
porta: u16,
host: []const u8,
max_conexoes: u32,
};
pub fn salvar(self: ConfigManager, config: Config) !void {
var file = try self.dir.createFile("config.ini", .{});
defer file.close();
var writer = file.writer();
try writer.print("[server]\n", .{});
try writer.print("porta={d}\n", .{config.porta});
try writer.print("host={s}\n", .{config.host});
try writer.print("max_conexoes={d}\n", .{config.max_conexoes});
}
pub fn carregar(self: ConfigManager, allocator: std.mem.Allocator) !Config {
var file = try self.dir.openFile("config.ini", .{});
defer file.close();
const conteudo = try file.readToEndAlloc(allocator, 4096);
defer allocator.free(conteudo);
// Parse simplificado
var config = Config{
.porta = 8080,
.host = "localhost",
.max_conexoes = 100,
};
var linhas = std.mem.splitScalar(u8, conteudo, '\n');
while (linhas.next()) |linha| {
if (std.mem.startsWith(u8, linha, "porta=")) {
config.porta = try std.fmt.parseInt(u16, linha["porta=".len..], 10);
} else if (std.mem.startsWith(u8, linha, "max_conexoes=")) {
config.max_conexoes = try std.fmt.parseInt(u32, linha["max_conexoes=".len..], 10);
}
}
return config;
}
};
test "integracao: salvar e carregar configuracao" {
// Arrange: diretorio temporario limpo automaticamente
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
const manager = ConfigManager{ .dir = tmp.dir };
// Act: salvar configuracao
try manager.salvar(.{
.porta = 3000,
.host = "0.0.0.0",
.max_conexoes = 500,
});
// Assert: carregar e verificar
const config = try manager.carregar(std.testing.allocator);
try std.testing.expectEqual(@as(u16, 3000), config.porta);
try std.testing.expectEqual(@as(u32, 500), config.max_conexoes);
}
test "integracao: config inexistente retorna erro" {
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
const manager = ConfigManager{ .dir = tmp.dir };
// Tentar carregar config que nao existe
const resultado = manager.carregar(std.testing.allocator);
try std.testing.expectError(error.FileNotFound, resultado);
}
Testando Operacoes de Arquivo
const LogWriter = struct {
arquivo: fs.File,
bytes_escritos: u64 = 0,
pub fn init(dir: fs.Dir) !LogWriter {
const arquivo = try dir.createFile("app.log", .{ .truncate = false });
return .{ .arquivo = arquivo };
}
pub fn escrever(self: *LogWriter, nivel: []const u8, mensagem: []const u8) !void {
var writer = self.arquivo.writer();
const timestamp = std.time.timestamp();
const n = try writer.print("[{d}] [{s}] {s}\n", .{ timestamp, nivel, mensagem });
self.bytes_escritos += n;
}
pub fn fechar(self: *LogWriter) void {
self.arquivo.close();
}
};
test "integracao: log writer escreve corretamente" {
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
// Escrever logs
{
var logger = try LogWriter.init(tmp.dir);
defer logger.fechar();
try logger.escrever("INFO", "Servidor iniciado");
try logger.escrever("WARN", "Memoria alta");
try logger.escrever("ERROR", "Conexao perdida");
try expect(logger.bytes_escritos > 0);
}
// Verificar conteudo do arquivo
const conteudo = try tmp.dir.readFileAlloc(std.testing.allocator, "app.log", 4096);
defer std.testing.allocator.free(conteudo);
try expect(std.mem.indexOf(u8, conteudo, "Servidor iniciado") != null);
try expect(std.mem.indexOf(u8, conteudo, "Memoria alta") != null);
try expect(std.mem.indexOf(u8, conteudo, "Conexao perdida") != null);
// Verificar que ha 3 linhas
var count: usize = 0;
for (conteudo) |c| {
if (c == '\n') count += 1;
}
try std.testing.expectEqual(@as(usize, 3), count);
}
Testando Networking
Servidor TCP de Teste
const std = @import("std");
const net = std.net;
/// Servidor echo simples para testes
const EchoServer = struct {
server: net.Server,
thread: ?std.Thread = null,
pub fn iniciar(porta: u16) !EchoServer {
const endereco = net.Address.initIp4(.{ 127, 0, 0, 1 }, porta);
var server = try endereco.listen(.{
.reuse_address = true,
});
return .{ .server = server };
}
pub fn rodarEmBackground(self: *EchoServer) !void {
self.thread = try std.Thread.spawn(.{}, aceitarConexoes, .{&self.server});
}
fn aceitarConexoes(server: *net.Server) void {
while (true) {
const conn = server.accept() catch break;
defer conn.stream.close();
var buf: [1024]u8 = undefined;
while (true) {
const n = conn.stream.read(&buf) catch break;
if (n == 0) break;
conn.stream.writeAll(buf[0..n]) catch break;
}
}
}
pub fn parar(self: *EchoServer) void {
self.server.deinit();
if (self.thread) |t| t.join();
}
pub fn porta(self: *EchoServer) u16 {
return self.server.listen_address.getPort();
}
};
test "integracao: echo server TCP" {
// Arrange: iniciar servidor na porta 0 (kernel escolhe porta livre)
var server = try EchoServer.iniciar(0);
defer server.parar();
try server.rodarEmBackground();
const porta_server = server.porta();
// Act: conectar como cliente
const endereco = net.Address.initIp4(.{ 127, 0, 0, 1 }, porta_server);
const stream = try net.tcpConnectToAddress(endereco);
defer stream.close();
// Enviar mensagem
try stream.writeAll("Ola Zig!");
// Receber resposta
var buf: [1024]u8 = undefined;
const n = try stream.read(&buf);
// Assert: servidor deve ecoar a mensagem
try std.testing.expectEqualStrings("Ola Zig!", buf[0..n]);
}
Testando HTTP Client
const std = @import("std");
/// Cliente HTTP simples
const HttpClient = struct {
allocator: std.mem.Allocator,
const Resposta = struct {
status: u16,
body: []const u8,
allocator: std.mem.Allocator,
pub fn deinit(self: *Resposta) void {
self.allocator.free(self.body);
}
};
pub fn get(self: HttpClient, url: []const u8) !Resposta {
const uri = try std.Uri.parse(url);
var client = std.http.Client{ .allocator = self.allocator };
defer client.deinit();
var buf: [4096]u8 = undefined;
var req = try client.open(.GET, uri, .{
.server_header_buffer = &buf,
});
defer req.deinit();
try req.send();
try req.wait();
const body = try req.reader().readAllAlloc(self.allocator, 1024 * 1024);
return .{
.status = @intFromEnum(req.response.status),
.body = body,
.allocator = self.allocator,
};
}
};
test "integracao: HTTP GET" {
// Este teste requer acesso a rede — pode ser marcado como skip
// em ambientes sem rede
const client = HttpClient{ .allocator = std.testing.allocator };
var resposta = client.get("http://httpbin.org/get") catch |err| {
// Se nao houver rede disponivel, pular o teste
if (err == error.ConnectionRefused or err == error.NetworkUnreachable) {
return;
}
return err;
};
defer resposta.deinit();
try std.testing.expectEqual(@as(u16, 200), resposta.status);
try std.testing.expect(resposta.body.len > 0);
}
Testando Subprocessos
const std = @import("std");
test "integracao: executar subprocesso" {
const allocator = std.testing.allocator;
// Executar um comando e capturar saida
var child = std.process.Child.init(
&.{ "echo", "Hello from Zig" },
allocator,
);
child.stdout_behavior = .Pipe;
try child.spawn();
const stdout = try child.stdout.?.reader().readAllAlloc(allocator, 4096);
defer allocator.free(stdout);
const resultado = try child.wait();
try std.testing.expectEqual(std.process.Child.Term{ .Exited = 0 }, resultado);
try std.testing.expectEqualStrings("Hello from Zig\n", stdout);
}
test "integracao: subprocesso com erro" {
const allocator = std.testing.allocator;
var child = std.process.Child.init(
&.{ "ls", "/diretorio/inexistente" },
allocator,
);
child.stderr_behavior = .Pipe;
try child.spawn();
const stderr = try child.stderr.?.reader().readAllAlloc(allocator, 4096);
defer allocator.free(stderr);
const resultado = try child.wait();
// ls retorna codigo de saida != 0 para diretorio inexistente
switch (resultado) {
.Exited => |code| try std.testing.expect(code != 0),
else => try std.testing.expect(false),
}
try std.testing.expect(stderr.len > 0);
}
Testes End-to-End
Testando uma Aplicacao Completa
const std = @import("std");
/// Aplicacao de key-value store com persistencia
const KVStore = struct {
dados: std.StringHashMap([]const u8),
dir: std.fs.Dir,
allocator: std.mem.Allocator,
pub fn init(allocator: std.mem.Allocator, dir: std.fs.Dir) KVStore {
return .{
.dados = std.StringHashMap([]const u8).init(allocator),
.dir = dir,
.allocator = allocator,
};
}
pub fn deinit(self: *KVStore) void {
var it = self.dados.iterator();
while (it.next()) |entry| {
self.allocator.free(entry.key_ptr.*);
self.allocator.free(entry.value_ptr.*);
}
self.dados.deinit();
}
pub fn set(self: *KVStore, chave: []const u8, valor: []const u8) !void {
const k = try self.allocator.dupe(u8, chave);
errdefer self.allocator.free(k);
const v = try self.allocator.dupe(u8, valor);
errdefer self.allocator.free(v);
if (self.dados.fetchPut(k, v)) |old| {
self.allocator.free(old.key);
self.allocator.free(old.value);
}
}
pub fn get(self: *KVStore, chave: []const u8) ?[]const u8 {
return self.dados.get(chave);
}
pub fn persistir(self: *KVStore) !void {
var file = try self.dir.createFile("store.dat", .{});
defer file.close();
var writer = file.writer();
var it = self.dados.iterator();
while (it.next()) |entry| {
const key_len: u32 = @intCast(entry.key_ptr.*.len);
const val_len: u32 = @intCast(entry.value_ptr.*.len);
try writer.writeInt(u32, key_len, .little);
try writer.writeAll(entry.key_ptr.*);
try writer.writeInt(u32, val_len, .little);
try writer.writeAll(entry.value_ptr.*);
}
}
pub fn restaurar(self: *KVStore) !void {
var file = self.dir.openFile("store.dat", .{}) catch |err| {
if (err == error.FileNotFound) return;
return err;
};
defer file.close();
var reader = file.reader();
while (true) {
const key_len = reader.readInt(u32, .little) catch break;
const key = try self.allocator.alloc(u8, key_len);
errdefer self.allocator.free(key);
try reader.readNoEof(key);
const val_len = try reader.readInt(u32, .little);
const val = try self.allocator.alloc(u8, val_len);
errdefer self.allocator.free(val);
try reader.readNoEof(val);
try self.dados.put(key, val);
}
}
};
test "e2e: KV store com persistencia" {
const allocator = std.testing.allocator;
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
// Fase 1: Criar e popular a store
{
var store = KVStore.init(allocator, tmp.dir);
defer store.deinit();
try store.set("nome", "Zig Brasil");
try store.set("versao", "0.14.0");
try store.set("linguagem", "Zig");
try std.testing.expectEqualStrings("Zig Brasil", store.get("nome").?);
// Persistir no disco
try store.persistir();
}
// Fase 2: Restaurar de uma nova instancia
{
var store = KVStore.init(allocator, tmp.dir);
defer store.deinit();
try store.restaurar();
// Verificar que todos os dados foram restaurados
try std.testing.expectEqualStrings("Zig Brasil", store.get("nome").?);
try std.testing.expectEqualStrings("0.14.0", store.get("versao").?);
try std.testing.expectEqualStrings("Zig", store.get("linguagem").?);
// Chave inexistente retorna null
try std.testing.expect(store.get("inexistente") == null);
}
}
Helpers para Testes de Integracao
Retry com Timeout
/// Repetir uma operacao ate sucesso ou timeout
fn retryAteTimeout(
comptime T: type,
func: fn () anyerror!T,
timeout_ms: u64,
) !T {
const inicio = std.time.milliTimestamp();
while (true) {
if (func()) |resultado| {
return resultado;
} else |_| {
const elapsed: u64 = @intCast(std.time.milliTimestamp() - @as(i64, @intCast(inicio)));
if (elapsed > timeout_ms) {
return error.Timeout;
}
std.time.sleep(100 * std.time.ns_per_ms);
}
}
}
test "integracao: retry com timeout" {
var contador: u32 = 0;
const resultado = try retryAteTimeout(u32, struct {
fn call() anyerror!u32 {
// Simular operacao que falha nas primeiras tentativas
return 42;
}
}.call, 5000);
_ = contador;
try std.testing.expectEqual(@as(u32, 42), resultado);
}
Conclusao
Testes de integracao sao essenciais para garantir que seu software funciona no mundo real — com discos, redes e processos reais. Zig facilita a escrita desses testes com ferramentas built-in como std.testing.tmpDir(), abstrações de rede e suporte a subprocessos. A chave e manter os testes reproduziveis, isolados e rapidos o suficiente para rodar frequentemente.
Proximo Artigo
No Artigo 5: CI/CD e Automacao, vamos automatizar a execucao de todos esses testes com GitHub Actions e pipelines de CI/CD.
Conteudo Relacionado
- Fuzz Testing em Zig — Artigo anterior
- CI/CD e Automacao — Proximo artigo
- File I/O em Zig — Tutorial de I/O
- Networking em Zig — Tutorial de networking
- API REST Completa com Zig — Projeto REST