zig-testing-integrado

Testing en Zig: pruebas integradas dentro del lenguaje

  • 4 min

El testing integrado de Zig es un sistema de pruebas que vive dentro del propio lenguaje y se ejecuta con zig test. En muchos lenguajes, las pruebas dependen de frameworks externos; Zig las trae de serie.

  • En Java/C++: Tienes que crear una carpeta tests/ paralela a src/, configurar CMake o Maven, y luchar con la visibilidad (¿cómo pruebo esta función privada?).
  • En Python/JS: Necesitas instalar frameworks externos (PyTest, Jest) y configurar el runner.

En Zig, el Testing es parte del lenguaje.

Zig parte de una filosofía simple: Si escribir un test es difícil o tedioso, no lo harás. Por eso, Zig te permite escribir los tests en el mismo archivo que tu código fuente, justo al lado de la función que estás probando.

El bloque test

Para escribir una prueba usamos la palabra clave test, un nombre descriptivo y un bloque de código.

const std = @import("std");

// Nuestra función de lógica de negocio
fn sumar(a: i32, b: i32) i32 {
    return a + b;
}

// El test puede vivir aquí mismo.
test "sumar funciona con positivos" {
    const resultado = sumar(2, 2);
    
    // Usamos 'expect' para validar
    try std.testing.expect(resultado == 4);
}

test "sumar funciona con negativos" {
    try std.testing.expect(sumar(-1, -1) == -2);
}
Copied!

Para ejecutar estos tests, no compilamos el programa normal. Usamos un comando especial:

zig test main.zig
Copied!

Si todo va bien, verás: All 2 tests passed.

Los tests no forman parte del ejecutable normal

Cuando ejecutas zig build-exe o zig build-obj, las declaraciones test se omiten de la compilación normal. Se analizan al construir pruebas mediante zig test o un paso de test de build.zig.

El espacio de nombres std.testing

La librería estándar nos ofrece varias aserciones útiles. Todas ellas devuelven un error si fallan, por lo que debemos marcar nuestros tests con try.

  • expect(condicion): La más básica. Falla si es false.
  • expectEqual(esperado, actual): Comprueba igualdad. Útil porque si falla, te dice “Expected 5, found 4”.
  • expectError(error_esperado, accion): Muy útil para probar nuestro sistema de errores !T.
  • expectEqualStrings(str1, str2): Para comparar slices de texto.
test "validar errores" {
    const resultado = funcionQueFalla();
    try std.testing.expectError(error.Oops, resultado);
}
Copied!

Probar funciones privadas

En Zig, como el test está en el mismo archivo (y por tanto, en el mismo ámbito o scope), los tests pueden acceder a las funciones privadas (fn sin pub).

// Función privada (no exportada)
fn logicaInternaCompleja(x: u32) u32 {
    return x * 2;
}

// Función pública
pub fn apiPublica(x: u32) u32 {
    return logicaInternaCompleja(x) + 1;
}

test "logica interna se comporta bien" {
    // Podemos llamar a la función privada directamente
    try std.testing.expectEqual(4, logicaInternaCompleja(2));
}
Copied!

Esto permite comprobar la implementación sin convertirla en parte de la API pública.

Detección de fugas de memoria

¿Recuerdas el DebugAllocator que vimos en el bloque de memoria?

Zig no asigna memoria por nosotros dentro de los tests. Cuando una prueba necesita un allocator, podemos usar std.testing.allocator, que detecta las fugas de las reservas realizadas a través de él.

test "detectar leak" {
    // Usamos el allocator de testing
    const list = try std.testing.allocator.create(i32);
    list.* = 10;
    
    // Uups, se nos olvidó: std.testing.allocator.destroy(list);
    // El test pasará las aserciones, PERO Zig fallará al final.
}
Copied!

Salida:

error: memory leaked
... traza de dónde se asignó la memoria ...
Copied!

Así las pruebas unitarias también pueden comprobar la propiedad y liberación de memoria.

Doctests

Un doctest es un bloque test cuyo nombre es el identificador de otra declaración, sin comillas. Sirve como ejemplo verificable y aparece asociado a esa declaración en la documentación generada.

/// Suma uno al valor recibido.
pub fn sumarUno(valor: i32) i32 {
    return valor + 1;
}

test sumarUno {
    try std.testing.expectEqual(42, sumarUno(41));
}
Copied!

Tests en proyectos con varios archivos

Si tienes muchos archivos, no quieres ejecutar zig test archivo1.zig, luego archivo2.zig, etc.

Zig analiza las declaraciones de forma perezosa. Para que la compilación raíz de tests descubra otros archivos, podemos importarlos desde un bloque test sin nombre.

main.zig:

test {
    // Esto fuerza al compilador a mirar dentro de estos archivos
    // y encontrar sus bloques de test.
    _ = @import("modelos/usuario.zig");
    _ = @import("utils/matematicas.zig");
    
}
Copied!