Testes de Integracao em Zig: I/O, Networking e End-to-End

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

AspectoUnit TestsIntegration Tests
EscopoFuncao/modulo individualMultiplos componentes
DependenciasMockadasReais
VelocidadeMilissegundosSegundos
IsolamentoTotalParcial
FocoLogica de negocioInteracao 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

Continue aprendendo Zig

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