4. Pruebas automatizadas
Temas del programa: 3.3
Qué es una prueba automatizada
Section titled “Qué es una prueba automatizada”Código que ejecuta tu código con datos conocidos y compara el resultado con lo esperado
Mismo resultado cada vez
Segundos no minutos
Nadie tiene que recordar ejecutarla
Unitaria o integración
Section titled “Unitaria o integración”| Unitaria | Integración | |
|---|---|---|
| Verifica | Una pieza aislada | Piezas reales juntas |
| Base de datos | No | Sí la de pruebas |
| Velocidad | Milisegundos | Más lenta |
| En Laravel | tests/Unit | tests/Feature |
Una unidad es la pieza más pequeña con una responsabilidad propia · un método con una regla
Toda prueba tiene tres partes
Section titled “Toda prueba tiene tres partes”Preparar → los datos y las piezas que la prueba necesitaActuar → llamar una sola vez al código que se pruebaVerificar → comparar el resultado con lo esperadoEn inglés Arrange Act Assert · si no puedes nombrar las tres partes la prueba hace demasiado
Ejemplo mínimo — la regla
Section titled “Ejemplo mínimo — la regla”// Ejemplo de aprendizaje · no forma parte de ReservaFICclass 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
Ejemplo mínimo — su prueba
Section titled “Ejemplo mínimo — su prueba”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_
Un positivo no basta
Section titled “Un positivo no basta”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'));}Hace lo que debe
Rechaza lo que debe
El valor justo en el límite
Ejecutar y leer el resultado
Section titled “Ejecutar y leer el resultado”php artisan test --filter=ReglaHorarioTestPASS Tests\Unit\ReglaHorarioTest acepta un fin posterior al inicio rechaza un fin anterior al inicio rechaza un fin igual al inicio
Tests: 3 passedSalida simplificada · ante una falla PHPUnit muestra lo esperado lo obtenido y la línea
Las aserciones que más usarás
Section titled “Las aserciones que más usarás”| Aserción | Verifica |
|---|---|
assertTrue y assertFalse | Un 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
Paso 2 — una unidad con dependencias
Section titled “Paso 2 — una unidad con dependencias”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
El repositorio falso
Section titled “El repositorio falso”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(); } };}Probar el camino feliz
Section titled “Probar el camino feliz”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
Probar que debe fallar
Section titled “Probar que debe fallar”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
Dobles de prueba
Section titled “Dobles de prueba”Objetos que ocupan el lugar de una dependencia real durante la prueba · lo viste en Pruebas de Software
| Doble | Qué hace |
|---|---|
| Dummy | Se pasa porque la firma lo exige · nadie lo usa |
| Stub | Devuelve respuestas preparadas |
| Spy | Registra cómo lo llamaron |
| Mock | Trae programadas las llamadas que deben ocurrir |
| Fake | Funciona pero simplificado · por ejemplo en memoria |
¿Qué doble es el repositorio falso?
Section titled “¿Qué doble es el repositorio falso?”existeConflicto() responde lo que la prueba decide
$creados recuerda qué le pidieron
Los nombres describen papeles no clases · un mismo objeto puede cumplir dos
Stub o mock — qué se verifica
Section titled “Stub o mock — qué se verifica”¿Qué resultado produjo? · excepción · valor devuelto · lo registrado
¿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
El mismo caso con un mock de PHPUnit
Section titled “El mismo caso con un mock de PHPUnit”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()
La expectativa es la aserción
Section titled “La expectativa es la aserción”$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
Un fake puede esconder el defecto
Section titled “Un fake puede esconder el defecto”Reescribe en PHP la condición de traslape · prueba la copia
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
Cuándo usar un doble y cuándo no
Section titled “Cuándo usar un doble y cuándo no”| Situación | Decisión |
|---|---|
| Correo o API de terceros | Doble · Mail::fake() Http::fake() |
| Aislar una regla de la base de datos | Stub del repositorio |
| Importa que algo no ocurra | Mock o spy |
| Integración con la base del proyecto | Base real · nunca un doble |
| La clase que se prueba | Nunca se sustituye |
08-ESTANDAR-PRUEBAS.md §6 · muchos mocks que verifican el orden interno atan la prueba a la implementación
Paso 3 — muchos casos una sola prueba
Section titled “Paso 3 — muchos casos una sola prueba”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): voidCada fila es un caso con nombre · la tabla de la Actividad A1 se vuelve código
Nombres que explican
Section titled “Nombres que explican”test1 · testPrestamo · testFunciona
test_responde_422_cuando_falta_fecha_fin
Cuando una prueba falla su nombre debe decirte qué regla se rompió sin abrir el archivo
Errores frecuentes al probar
Section titled “Errores frecuentes al probar”| Error | Por qué es un problema |
|---|---|
| Prueba sin aserción | Siempre pasa |
| Una prueba depende de otra | Falla según el orden · STD-TEST-03 |
| Probar el framework | Verificar que Eloquent guarda no prueba tu regla |
| Solo el caso feliz | Los defectos viven en los bordes |
| Nunca verla fallar | Puede estar pasando por la razón equivocada |
Buenas prácticas — F.I.R.S.T.
Section titled “Buenas prácticas — F.I.R.S.T.”| Significa | En el proyecto | |
|---|---|---|
| Fast | Milisegundos | Unitarias sin base de datos |
| Independent | No depende de otra | STD-TEST-03 |
| Repeatable | Mismo resultado siempre | Sin red ni fecha actual sin control |
| Self-validating | Pasa o falla sola | Aserciones · nunca dd() para mirar |
| Timely | Junto con el código | Sin pruebas la historia no está terminada |
Clean Code · Martin 2008 · retoma las buenas prácticas de Pruebas de Software
Seis prácticas concretas
Section titled “Seis prácticas concretas”Varias aserciones sí · dos reglas no
Nada de if o for · usa data provider
assertSame(120 ...) · no recalcules con la misma fórmula
Fechas fijas · travelTo() si la regla usa now()
Métodos públicos · no privados
El dato que decide se lee en la prueba
La lección del defecto
Section titled “La lección del defecto”Usa un repositorio falso que no ejecuta SQL y pasa con el defecto
Ejecuta la consulta real contra PostgreSQL y falla con el defecto
Ocho casos tres fallan
Section titled “Ocho casos tres fallan”| Nuevo frente a 10:00 a 12:00 | Esperado | Bloque II |
|---|---|---|
| 10:30 a 11:30 contenido | Conflicto | Sin conflicto |
| 08:00 a 10:00 contiguo | Sin conflicto | Conflicto |
| 12:00 a 14:00 contiguo | Sin conflicto | Conflicto |
Los defectos viven en los bordes
La corrección
Section titled “La corrección”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
Ver la prueba fallar primero
Section titled “Ver la prueba fallar primero”Pasos para reproducir
Commit antes de corregir
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
Relación con el laboratorio
Section titled “Relación con el laboratorio”Este tema se practica en el Laboratorio 3 — Pruebas unitarias y de integración.
Qué sigue
Section titled “Qué sigue”Continúa con 5. Datos de prueba.

