Skip to content

4. Pruebas automatizadas

Temas del programa: 3.3

Código que ejecuta tu código con datos conocidos y compara el resultado con lo esperado

Repetible

Mismo resultado cada vez

Rápida

Segundos no minutos

Automática

Nadie tiene que recordar ejecutarla

UnitariaIntegración
VerificaUna pieza aisladaPiezas reales juntas
Base de datosNoSí la de pruebas
VelocidadMilisegundosMás lenta
En Laraveltests/Unittests/Feature

Una unidad es la pieza más pequeña con una responsabilidad propia · un método con una regla

Preparar → los datos y las piezas que la prueba necesita
Actuar → llamar una sola vez al código que se prueba
Verificar → comparar el resultado con lo esperado

En inglés Arrange Act Assert · si no puedes nombrar las tres partes la prueba hace demasiado

// Ejemplo de aprendizaje · no forma parte de ReservaFIC
class ReglaHorario
{
public static function esValido(string $inicio, string $fin): bool
{
return strtotime($fin) > strtotime($inicio);
}
}

Sin base de datos sin HTTP sin Laravel · la forma más simple de unidad

use PHPUnit\Framework\TestCase;
class ReglaHorarioTest extends TestCase
{
public function test_acepta_un_fin_posterior_al_inicio(): void
{
$inicio = '2026-11-10 10:00'; // Preparar
$fin = '2026-11-10 12:00';
$resultado = ReglaHorario::esValido($inicio, $fin); // Actuar
$this->assertTrue($resultado); // Verificar
}
}

tests/Unit · extiende PHPUnit\Framework\TestCase · el método empieza con test_

public function test_rechaza_un_fin_anterior_al_inicio(): void
{
$this->assertFalse(ReglaHorario::esValido('2026-11-10 12:00', '2026-11-10 10:00'));
}
public function test_rechaza_un_fin_igual_al_inicio(): void
{
$this->assertFalse(ReglaHorario::esValido('2026-11-10 10:00', '2026-11-10 10:00'));
}
Positivo

Hace lo que debe

Negativo

Rechaza lo que debe

Borde

El valor justo en el límite

Terminal window
php artisan test --filter=ReglaHorarioTest
PASS Tests\Unit\ReglaHorarioTest
acepta un fin posterior al inicio
rechaza un fin anterior al inicio
rechaza un fin igual al inicio
Tests: 3 passed

Salida simplificada · ante una falla PHPUnit muestra lo esperado lo obtenido y la línea

AserciónVerifica
assertTrue y assertFalseUn resultado booleano
assertSame($esperado, $real)Valor y tipo idénticos
assertCount(1, $lista)Cuántos elementos hay
expectException(Clase::class)Que se lance una excepción
assertDatabaseHas('tabla', [...])Que una fila existe · solo integración

Una prueba sin aserción siempre pasa y no demuestra nada

PrestamoService necesita un repositorio que sí usa la base de datos · ¿cómo se prueba aislado?

Prueba ──► PrestamoService ──► PrestamoRepositoryInterface
├── EloquentPrestamoRepository (producción)
└── repositorio falso (prueba)

Por esto el equipo conserva el Repository · ADR-002

private function repositorioFalso(bool $hayConflicto): PrestamoRepositoryInterface
{
return new class($hayConflicto) implements PrestamoRepositoryInterface {
public array $creados = [];
public function __construct(private bool $hayConflicto) {}
public function existeConflicto(array $datos): bool
{
return $this->hayConflicto; // responde lo que la prueba decide
}
public function crear(array $datos): Prestamo
{
$this->creados[] = $datos; // recuerda qué le pidieron
return new Prestamo();
}
};
}
public function test_crea_el_prestamo_cuando_no_hay_conflicto(): void
{
$repositorio = $this->repositorioFalso(hayConflicto: false);
(new PrestamoService($repositorio))->crear(['equipo_id' => 1, 'user_id' => 7]);
$this->assertCount(1, $repositorio->creados);
}

No hay base de datos · la prueba corre en milisegundos

public function test_lanza_excepcion_y_no_crea_cuando_hay_conflicto(): void
{
$repositorio = $this->repositorioFalso(hayConflicto: true);
try {
(new PrestamoService($repositorio))->crear(['equipo_id' => 1, 'user_id' => 7]);
$this->fail('Se esperaba ConflictoDeReservaException.');
} catch (ConflictoDeReservaException $e) {
$this->assertSame(1, $e->equipoId);
$this->assertCount(0, $repositorio->creados);
}
}

Verifica dos cosas — que se lanzó la excepción correcta y que no se guardó nada

Objetos que ocupan el lugar de una dependencia real durante la prueba · lo viste en Pruebas de Software

DobleQué hace
DummySe pasa porque la firma lo exige · nadie lo usa
StubDevuelve respuestas preparadas
SpyRegistra cómo lo llamaron
MockTrae programadas las llamadas que deben ocurrir
FakeFunciona pero simplificado · por ejemplo en memoria
Stub

existeConflicto() responde lo que la prueba decide

Spy

$creados recuerda qué le pidieron

Los nombres describen papeles no clases · un mismo objeto puede cumplir dos

Stub · verificación de estado

¿Qué resultado produjo? · excepción · valor devuelto · lo registrado

Mock · verificación de interacción

¿Se llamó o no se llamó? · cuántas veces · con qué argumentos

La diferencia importante no es el nombre sino lo que la prueba comprueba al final

public function test_no_guarda_nada_cuando_hay_conflicto(): void
{
$repositorio = $this->createMock(PrestamoRepositoryInterface::class);
$repositorio->method('existeConflicto')->willReturn(true); // papel de stub
$repositorio->expects($this->never())->method('crear'); // papel de mock
$this->expectException(ConflictoDeReservaException::class);
(new PrestamoService($repositorio))->crear(['equipo_id' => 1, 'user_id' => 7]);
}

createStub() solo responde · createMock() además declara expectativas con expects()

$repositorio->expects($this->once())
->method('crear')
->with($datos)
->willReturn(new Prestamo());
(new PrestamoService($repositorio))->crear($datos);

No hay assert · PHPUnit falla la prueba si crear() no se llamó una vez con esos datos

Repositorio en memoria

Reescribe en PHP la condición de traslape · prueba la copia

PostgreSQL de pruebas

Ejecuta la consulta real · prueba el original

Si la consulta SQL sigue mal el fake pasa igual · por eso la condición se prueba contra la base real

SituaciónDecisión
Correo o API de tercerosDoble · Mail::fake() Http::fake()
Aislar una regla de la base de datosStub del repositorio
Importa que algo no ocurraMock o spy
Integración con la base del proyectoBase real · nunca un doble
La clase que se pruebaNunca se sustituye

08-ESTANDAR-PRUEBAS.md §6 · muchos mocks que verifican el orden interno atan la prueba a la implementación

public static function casos(): array
{
return [
'mismo horario' => ['10:00', '12:00', true],
'contenido dentro del existente' => ['10:30', '11:30', true],
'termina justo cuando empieza el existente' => ['08:00', '10:00', false],
'empieza justo cuando termina el existente' => ['12:00', '14:00', false],
];
}
#[DataProvider('casos')]
public function test_detecta_traslape_de_horarios(string $inicio, string $fin, bool $esperado): void

Cada fila es un caso con nombre · la tabla de la Actividad A1 se vuelve código

Así no

test1 · testPrestamo · testFunciona

Así sí

test_responde_422_cuando_falta_fecha_fin

Cuando una prueba falla su nombre debe decirte qué regla se rompió sin abrir el archivo

ErrorPor qué es un problema
Prueba sin aserciónSiempre pasa
Una prueba depende de otraFalla según el orden · STD-TEST-03
Probar el frameworkVerificar que Eloquent guarda no prueba tu regla
Solo el caso felizLos defectos viven en los bordes
Nunca verla fallarPuede estar pasando por la razón equivocada
SignificaEn el proyecto
FastMilisegundosUnitarias sin base de datos
IndependentNo depende de otraSTD-TEST-03
RepeatableMismo resultado siempreSin red ni fecha actual sin control
Self-validatingPasa o falla solaAserciones · nunca dd() para mirar
TimelyJunto con el códigoSin pruebas la historia no está terminada

Clean Code · Martin 2008 · retoma las buenas prácticas de Pruebas de Software

Un comportamiento por prueba

Varias aserciones sí · dos reglas no

Sin lógica en la prueba

Nada de if o for · usa data provider

Esperados literales

assertSame(120 ...) · no recalcules con la misma fórmula

Controla el tiempo

Fechas fijas · travelTo() si la regla usa now()

Por la puerta principal

Métodos públicos · no privados

Preparación a la vista

El dato que decide se lee en la prueba

Prueba unitaria del Service

Usa un repositorio falso que no ejecuta SQL y pasa con el defecto

Prueba de integración

Ejecuta la consulta real contra PostgreSQL y falla con el defecto

Nuevo frente a 10:00 a 12:00EsperadoBloque II
10:30 a 11:30 contenidoConflictoSin conflicto
08:00 a 10:00 contiguoSin conflictoConflicto
12:00 a 14:00 contiguoSin conflictoConflicto

Los defectos viven en los bordes

Prestamo::where('equipo_id', $datos['equipo_id'])
->where('fecha_inicio', '<', $datos['fecha_fin'])
->where('fecha_fin', '>', $datos['fecha_inicio'])
->exists();

Dos intervalos se traslapan si cada uno empieza antes de que termine el otro

El Service no cambió — el Repository aisló el defecto en un solo lugar

1 Issue

Pasos para reproducir

2 Prueba en rojo

Commit antes de corregir

3 Corrección

La misma prueba en verde

Una prueba que nunca viste fallar puede estar pasando por la razón equivocada

Actividad A4 — matriz de casos positivos negativos y de borde

Continúa con 5. Datos de prueba.