Patrones de Pruebas en Perl
Estrategias exhaustivas para pruebas en aplicaciones Perl usando Test2::V0, Test::More, prove y metodología TDD.
Cuándo activarlo
- Escribir código Perl nuevo (sigue TDD: rojo, verde, refactorizar)
- Diseñar suites de prueba para módulos o aplicaciones Perl
- Revisar la cobertura de pruebas Perl
- Configurar infraestructura de pruebas Perl
- Migrar pruebas de Test::More a Test2::V0
- Depurar pruebas Perl que fallan
Flujo de trabajo TDD
Sigue siempre el ciclo ROJO-VERDE-REFACTORIZAR.
# Paso 1: ROJO — Escribe una prueba que falle
# t/unit/calculator.t
use v5.36;
use Test2::V0;
use lib 'lib';
use Calculator;
subtest 'adición' => sub {
my $calc = Calculator->new;
is($calc->add(2, 3), 5, 'suma dos números');
is($calc->add(-1, 1), 0, 'maneja negativos');
};
done_testing;
# Paso 2: VERDE — Escribe la implementación mínima
# lib/Calculator.pm
package Calculator;
use v5.36;
use Moo;
sub add($self, $a, $b) {
return $a + $b;
}
1;
# Paso 3: REFACTORIZAR — Mejora mientras las pruebas sigan en verde
# Ejecuta: prove -lv t/unit/calculator.t
Fundamentos de Test::More
El módulo de pruebas estándar de Perl — ampliamente usado, incluido en el núcleo.
Aserciones básicas
use v5.36;
use Test::More;
# Plan anticipado o usar done_testing
# plan tests => 5; # Plan fijo (opcional)
# Igualdad
is($resultado, 42, 'devuelve el valor correcto');
isnt($resultado, 0, 'no es cero');
# Booleano
ok($usuario->is_active, 'el usuario está activo');
ok(!$usuario->is_banned, 'el usuario no está baneado');
# Comparación profunda
is_deeply(
$obtenido,
{ nombre => 'Ana', roles => ['admin'] },
'devuelve la estructura esperada'
);
# Coincidencia de patrones
like($error, qr/no encontrado/i, 'el error menciona "no encontrado"');
unlike($salida, qr/contraseña/, 'la salida oculta la contraseña');
# Verificación de tipo
isa_ok($obj, 'MiApp::Usuario');
can_ok($obj, 'guardar', 'eliminar');
done_testing;
SKIP y TODO
use v5.36;
use Test::More;
# Saltar pruebas condicionalmente
SKIP: {
skip 'No hay base de datos configurada', 2 a menos que $ENV{DB_PRUEBAS};
my $db = conectar_bd();
ok($db->ping, 'la base de datos es accesible');
is($db->version, '15', 'versión correcta de PostgreSQL');
}
# Marcar fallos esperados
TODO: {
local $TODO = 'Caché aún no implementada';
is($cache->get('clave'), 'valor', 'la caché devuelve el valor');
}
done_testing;
Test2::V0 Framework Moderno
Test2::V0 es el reemplazo moderno de Test::More — aserciones más ricas, mejor diagnóstico y extensible.
¿Por qué Test2?
- Comparación profunda superior con constructores hash/array
- Mejor salida de diagnóstico en fallos
- Subtests con ámbito más limpio
- Extensible mediante plugins Test2::Tools::*
- Compatible hacia atrás con pruebas de Test::More
Comparación profunda con constructores
use v5.36;
use Test2::V0;
# Constructor hash — verifica estructura parcial
is(
$usuario->to_hash,
hash {
field nombre => 'Ana';
field email => match(qr/\@ejemplo\.com$/);
field edad => validator(sub { $_ >= 18 });
# Ignorar otros campos
etc();
},
'el usuario tiene los campos esperados'
);
# Constructor array
is(
$resultado,
array {
item 'primero';
item match(qr/^segundo/);
item DNE(); # Does Not Exist — verifica que no hay elementos extra
},
'el resultado coincide con la lista esperada'
);
# Bag — comparación sin importar el orden
is(
$etiquetas,
bag {
item 'perl';
item 'pruebas';
item 'tdd';
},
'tiene todas las etiquetas requeridas sin importar el orden'
);
Subtests
use v5.36;
use Test2::V0;
subtest 'Creación de usuario' => sub {
my $usuario = Usuario->new(nombre => 'Ana', email => '[email protected]');
ok($usuario, 'objeto usuario creado');
is($usuario->nombre, 'Ana', 'el nombre está establecido');
is($usuario->email, '[email protected]', 'el email está establecido');
};
subtest 'Validación de usuario' => sub {
my $avisos = warns {
Usuario->new(nombre => '', email => 'malo');
};
ok($avisos, 'advierte sobre datos inválidos');
};
done_testing;
Pruebas de excepciones con Test2
use v5.36;
use Test2::V0;
# Probar que el código muere
like(
dies { dividir(10, 0) },
qr/División por cero/,
'muere en división por cero'
);
# Probar que el código vive
ok(lives { dividir(10, 2) }, 'la división tiene éxito') o note($@);
# Patrón combinado
subtest 'manejo de errores' => sub {
ok(lives { parsear_config('valido.json') }, 'configuración válida se parsea');
like(
dies { parsear_config('falta.json') },
qr/No se puede abrir/,
'archivo faltante muere con mensaje'
);
};
done_testing;
Organización de pruebas y prove
Estructura de directorios
t/
├── 00-carga.t # Verifica que los módulos compilan
├── 01-basico.t # Funcionalidad principal
├── unit/
│ ├── config.t # Pruebas unitarias por módulo
│ ├── usuario.t
│ └── util.t
├── integration/
│ ├── base_de_datos.t
│ └── api.t
├── lib/
│ └── AyudantePruebas.pm # Utilidades compartidas para pruebas
└── fixtures/
├── config.json # Archivos de datos de prueba
└── usuarios.csv
Comandos prove
# Ejecutar todas las pruebas
prove -l t/
# Salida verbose
prove -lv t/
# Ejecutar una prueba específica
prove -lv t/unit/usuario.t
# Búsqueda recursiva
prove -lr t/
# Ejecución paralela (8 trabajos)
prove -lr -j8 t/
# Ejecutar solo pruebas que fallaron en la última ejecución
prove -l --state=failed t/
# Salida coloreada con temporizador
prove -l --color --timer t/
# Salida TAP para CI
prove -l --formatter TAP::Formatter::JUnit t/ > resultados.xml
Configuración .proverc
-l
--color
--timer
-r
-j4
--state=save
Fixtures y Configuración/Limpieza
Aislamiento con subtests
use v5.36;
use Test2::V0;
use File::Temp qw(tempdir);
use Path::Tiny;
subtest 'procesamiento de archivos' => sub {
# Configuración
my $dir = tempdir(LIMPIEZA => 1);
my $archivo = path($dir, 'entrada.txt');
$archivo->spew_utf8("línea1\nlínea2\nlínea3\n");
# Prueba
my $resultado = procesar_archivo("$archivo");
is($resultado->{conteo_lineas}, 3, 'cuenta líneas');
# La limpieza ocurre automáticamente (LIMPIEZA => 1)
};
Ayudantes de prueba compartidos
Coloca ayudantes reutilizables en t/lib/AyudantePruebas.pm y cárgalos con use lib 't/lib'. Exporta funciones fábrica como crear_bd_prueba(), crear_dir_temporal(), y ruta_fixture() mediante Exporter.
Mocking
Test::MockModule
use v5.36;
use Test2::V0;
use Test::MockModule;
subtest 'simular API externa' => sub {
my $mock = Test::MockModule->new('MiApp::API');
# Bien: Mock devuelve datos controlados
$mock->mock(obtener_usuario => sub ($self, $id) {
return { id => $id, nombre => 'Usuario Simulado', email => '[email protected]' };
});
my $api = MiApp::API->new;
my $usuario = $api->obtener_usuario(42);
is($usuario->{nombre}, 'Usuario Simulado', 'devuelve usuario simulado');
# Verificar conteo de llamadas
my $conteo_llamadas = 0;
$mock->mock(obtener_usuario => sub { $conteo_llamadas++; return {} });
$api->obtener_usuario(1);
$api->obtener_usuario(2);
is($conteo_llamadas, 2, 'obtener_usuario fue llamado dos veces');
# El mock se restaura automáticamente cuando $mock sale del ámbito
};
# Mal: Monkey-patching sin restauración
# *MiApp::API::obtener_usuario = sub { ... }; # NUNCA — se filtra entre pruebas
Para objetos mock ligeros, usa Test::MockObject para crear dobles de prueba inyectables con ->mock() y verifica llamadas con ->called_ok().
Cobertura con Devel::Cover
Ejecutar cobertura
# Reporte básico de cobertura
cover -test
# O paso a paso
perl -MDevel::Cover -Ilib t/unit/usuario.t
cover
# Reporte HTML
cover -report html
open cover_db/coverage.html
# Umbrales específicos
cover -test -report text | grep 'Total'
# Amigable para CI: falla si está bajo el umbral
cover -test && cover -report text -select '^lib/' \
| perl -ne 'if (/Total.*?(\d+\.\d+)/) { exit 1 if $1 < 80 }'
Pruebas de integración
Usa SQLite en memoria para pruebas de base de datos, simula HTTP::Tiny para pruebas de API.
use v5.36;
use Test2::V0;
use DBI;
subtest 'integración con base de datos' => sub {
my $dbh = DBI->connect('dbi:SQLite:dbname=:memory:', '', '', {
RaiseError => 1,
});
$dbh->do('CREATE TABLE usuarios (id INTEGER PRIMARY KEY, nombre TEXT)');
$dbh->prepare('INSERT INTO usuarios (nombre) VALUES (?)')->execute('Ana');
my $fila = $dbh->selectrow_hashref('SELECT * FROM usuarios WHERE nombre = ?', undef, 'Ana');
is($fila->{nombre}, 'Ana', 'usuario insertado y recuperado');
};
done_testing;
Buenas prácticas
SÍ HACER
- Seguir TDD: Escribe las pruebas antes de la implementación (rojo-verde-refactorizar)
- Usar Test2::V0: Aserciones modernas, mejor diagnóstico
- Usar subtests: Agrupa aserciones relacionadas, aísla el estado
- Simular dependencias externas: Red, base de datos, sistema de archivos
- Usar
prove -l: Incluye siempre lib/ en@INC - Nombrar pruebas claramente:
'inicio de sesión con contraseña inválida falla' - Probar casos límite: Cadenas vacías, undef, cero, valores frontera
- Apuntar a 80%+ de cobertura: Enfócate en las rutas de lógica de negocio
- Mantener pruebas rápidas: Simula E/S, usa bases de datos en memoria
NO HACER
- No probar la implementación: Prueba comportamiento y salida, no detalles internos
- No compartir estado entre subtests: Cada subtest debe ser independiente
- No omitir
done_testing: Asegura que todas las pruebas planificadas se ejecutaron - No sobresimular: Simula solo los límites, no el código bajo prueba
- No usar
Test::Morepara proyectos nuevos: Prefiere Test2::V0 - No ignorar fallos en pruebas: Todas las pruebas deben pasar antes de fusionar
- No probar módulos CPAN: Confía en que las bibliotecas funcionan correctamente
- No escribir pruebas frágiles: Evita coincidencias de cadenas demasiado específicas
Referencia rápida
| Tarea | Comando / Patrón |
|---|---|
| Ejecutar todas las pruebas | prove -lr t/ |
| Ejecutar una prueba verbose | prove -lv t/unit/usuario.t |
| Ejecución paralela | prove -lr -j8 t/ |
| Reporte de cobertura | cover -test && cover -report html |
| Probar igualdad | is($obtenido, $esperado, 'etiqueta') |
| Comparación profunda | is($obtenido, hash { field k => 'v'; etc() }, 'etiqueta') |
| Probar excepción | like(dies { ... }, qr/msg/, 'etiqueta') |
| Probar sin excepción | ok(lives { ... }, 'etiqueta') |
| Simular un método | Test::MockModule->new('Pkg')->mock(m => sub { ... }) |
| Saltar pruebas | SKIP: { skip 'razón', $cuenta a menos que $cond; ... } |
| Pruebas TODO | TODO: { local $TODO = 'razón'; ... } |
Errores comunes
Olvidar done_testing
# Mal: El archivo de prueba se ejecuta pero no verifica que todas las pruebas se ejecuten
use Test2::V0;
is(1, 1, 'funciona');
# Falta done_testing — errores silenciosos si el código de prueba se salta
# Bien: Siempre terminar con done_testing
use Test2::V0;
is(1, 1, 'funciona');
done_testing;
Falta el flag -l
# Mal: Módulos en lib/ no encontrados
prove t/unit/usuario.t
# No se puede localizar MiApp/Usuario.pm en @INC
# Bien: Incluir lib/ en @INC
prove -l t/unit/usuario.t
Sobresimulación
Simula la dependencia, no el código bajo prueba. Si tu prueba solo verifica que un mock devuelve lo que le dijiste, no prueba nada.
Contaminación de pruebas
Usa variables my dentro de los subtests — nunca our — para evitar que el estado se filtre entre pruebas.
Recuerda: Las pruebas son tu red de seguridad. Mantenlas rápidas, enfocadas e independientes. Usa Test2::V0 para proyectos nuevos, prove para ejecutar y Devel::Cover para la responsabilidad.