PHP
Tutorial PHP: Test doubles (Mocks, Stubs, Fakes, Spies)
PHP: Aprende a usar dobles de prueba para simplificar pruebas unitarias en PHP con PHPUnit y asegurar la calidad del código.
Aprende PHP GRATIS y certifícateUso de dobles de prueba para aislar dependencias
Al realizar pruebas unitarias, es fundamental aislar el código bajo prueba de sus dependencias externas. Esto se logra mediante el uso de dobles de prueba, que son objetos simulados que reemplazan las dependencias reales durante la ejecución de los tests. De esta manera, se garantiza que las pruebas sean deterministas y se enfoquen únicamente en la lógica interna del componente evaluado.
Existen diferentes tipos de dobles de prueba, como mocks, stubs, fakes y spies, cada uno con propósitos específicos:
- Stubs: proporcionan respuestas predefinidas a llamadas realizadas durante la prueba, permitiendo controlar el flujo de ejecución.
- Mocks: permiten verificar si ciertos métodos fueron invocados con parámetros específicos, ayudando a validar interacciones entre objetos.
- Fakes: son implementaciones simplificadas de componentes reales, utilizadas para pruebas que requieren funcionalidad funcional pero no completa.
- Spies: registran información sobre las interacciones que se producen, lo que permite inspeccionar posteriormente cómo se utilizaron.
En PHP, la herramienta PHPUnit facilita la creación y manejo de dobles de prueba. A continuación, se muestra un ejemplo de cómo utilizar un mock para aislar una dependencia:
<?php
use PHPUnit\Framework\TestCase;
class ServicioTest extends TestCase
{
public function testProcesarDatos()
{
$repositorioMock = $this->createMock(RepositorioInterface::class);
$repositorioMock->expects($this->once())
->method('obtenerDatos')
->willReturn(['dato1', 'dato2']);
$servicio = new Servicio($repositorioMock);
$resultado = $servicio->procesarDatos();
$this->assertEquals('procesado', $resultado);
}
}
En este ejemplo, se crea un mock de la interfaz RepositorioInterface
, asegurando que el método obtenerDatos
sea llamado exactamente una vez y que devuelva un array predefinido. Esto permite probar la clase Servicio
sin depender de la implementación real del repositorio, garantizando que la prueba sea fiable y enfocada.
Los stubs se utilizan cuando simplemente se necesita una respuesta controlada sin verificar interacciones. Por ejemplo:
<?php
$repositorioStub = $this->createStub(RepositorioInterface::class);
$repositorioStub->method('obtenerConfiguracion')
->willReturn(['config' => 'valor']);
Aquí, el stub asegura que el método obtenerConfiguracion
siempre devuelve un valor específico, útil para pruebas que no involucran lógica compleja de la dependencia.
El uso de fakes es común cuando se reemplazan componentes como bases de datos o servicios externos por implementaciones en memoria o simplificadas. Esto mejora la eficiencia de las pruebas y evita dependencias pesadas. Por ejemplo, implementar un repositorio en memoria:
<?php
class RepositorioFake implements RepositorioInterface
{
private $datos = [];
public function guardar($registro)
{
$this->datos[] = $registro;
}
public function obtenerTodos()
{
return $this->datos;
}
}
Los spies son útiles para verificar detalles sobre cómo se utilizaron las dependencias. Por ejemplo, contar cuántas veces se llamó a un método o con qué parámetros:
<?php
class LoggerSpy implements LoggerInterface
{
public $mensajes = [];
public function log($nivel, $mensaje)
{
$this->mensajes[] = ['nivel' => $nivel, 'mensaje' => $mensaje];
}
}
Al final de la prueba, se puede inspeccionar $loggerSpy->mensajes
para verificar que se registraron los mensajes esperados.
La implementación de dobles de prueba requiere un diseño del código que favorezca la inyección de dependencias. Al pasar las dependencias como parámetros (preferiblemente mediante interfaces), se facilita su reemplazo por dobles durante las pruebas. Esto no solo mejora la testabilidad del código, sino que también promueve principios de buen diseño como la inversión de dependencias.
Finalmente, es importante hacer un uso adecuado y consciente de cada tipo de doble de prueba. Comprender sus diferencias y aplicarlos correctamente contribuye a tener una suite de pruebas más robusta y mantenible, lo que es esencial en proyectos de desarrollo profesional.
Ejemplos prácticos con PHPUnit o librerías complementarias
Para comprender el uso de dobles de prueba en PHPUnit, desarrollaremos ejemplos prácticos que ilustran cómo emplear mocks, stubs, fakes y spies en situaciones comunes de desarrollo sin dependencias externas.
Imaginemos que tenemos una clase ClienteApi
que realiza peticiones a una API externa para obtener información de usuarios:
<?php
class ClienteApi
{
private $httpClient;
public function __construct(HttpClientInterface $httpClient)
{
$this->httpClient = $httpClient;
}
public function obtenerUsuario($id)
{
$respuesta = $this->httpClient->get("https://api.ejemplo.com/usuarios/{$id}");
return json_decode($respuesta, true);
}
}
Para probar el método obtenerUsuario
sin realizar llamadas reales a la API, creamos un mock del cliente HTTP. Esto nos permite simular la respuesta y controlar el comportamiento del test:
<?php
use PHPUnit\Framework\TestCase;
class ClienteApiTest extends TestCase
{
public function testObtenerUsuario()
{
$httpClientMock = $this->createMock(HttpClientInterface::class);
$httpClientMock->expects($this->once())
->method('get')
->with('https://api.ejemplo.com/usuarios/42')
->willReturn('{"id":42,"nombre":"Juan Pérez"}');
$clienteApi = new ClienteApi($httpClientMock);
$usuario = $clienteApi->obtenerUsuario(42);
$this->assertEquals(42, $usuario['id']);
$this->assertEquals('Juan Pérez', $usuario['nombre']);
}
}
En este ejemplo, el mock de HttpClientInterface
nos permite simular la respuesta de la API, asegurando que el test sea predecible y no dependa de factores externos.
Supongamos que tenemos una clase CalculadoraImpuestos
que depende de un servicio TipoCambio
para calcular impuestos en diferentes monedas:
<?php
class CalculadoraImpuestos
{
private $tipoCambio;
public function __construct(TipoCambioInterface $tipoCambio)
{
$this->tipoCambio = $tipoCambio;
}
public function calcular($monto, $moneda)
{
$tasa = $this->tipoCambio->obtenerTasa($moneda);
return $monto * 0.21 * $tasa;
}
}
Para probar CalculadoraImpuestos
sin depender del servicio real, utilizamos un stub que devuelve un tipo de cambio fijo:
<?php
use PHPUnit\Framework\TestCase;
class CalculadoraImpuestosTest extends TestCase
{
public function testCalcularImpuestoEnUSD()
{
$tipoCambioStub = $this->createStub(TipoCambioInterface::class);
$tipoCambioStub->method('obtenerTasa')
->willReturn(1.2);
$calculadora = new CalculadoraImpuestos($tipoCambioStub);
$resultado = $calculadora->calcular(100, 'USD');
$this->assertEquals(25.2, $resultado);
}
}
Con el stub, podemos controlar el valor devuelto por obtenerTasa
, permitiendo verificar el resultado de forma determinista.
Si necesitamos simular una base de datos, podemos implementar un fake que almacene los datos en memoria:
<?php
class RepositorioUsuariosFake implements RepositorioUsuariosInterface
{
private $usuarios = [];
public function guardar(Usuario $usuario)
{
$this->usuarios[$usuario->getId()] = $usuario;
}
public function obtenerPorId($id)
{
return $this->usuarios[$id] ?? null;
}
}
En nuestros tests, utilizamos el fake para evitar interactuar con una base de datos real:
<?php
use PHPUnit\Framework\TestCase;
class GestorUsuariosTest extends TestCase
{
public function testCrearUsuario()
{
$repositorio = new RepositorioUsuariosFake();
$gestor = new GestorUsuarios($repositorio);
$gestor->crearUsuario(1, 'Ana Gómez');
$usuario = $repositorio->obtenerPorId(1);
$this->assertEquals('Ana Gómez', $usuario->getNombre());
}
}
El fake nos permite realizar pruebas rápidas y sin dependencias pesadas.
Para verificar interacciones, usamos spies. Consideremos una clase ControladorNotificaciones
que envía emails:
<?php
class ControladorNotificaciones
{
private $emailService;
public function __construct(EmailServiceInterface $emailService)
{
$this->emailService = $emailService;
}
public function notificar($destinatario, $mensaje)
{
$this->emailService->enviar($destinatario, $mensaje);
}
}
En el test, utilizamos un spy para asegurarnos de que el método enviar
se llama correctamente:
<?php
use PHPUnit\Framework\TestCase;
class ControladorNotificacionesTest extends TestCase
{
public function testNotificarEnvíaEmail()
{
$emailServiceSpy = $this->createMock(EmailServiceInterface::class);
$emailServiceSpy->expects($this->once())
->method('enviar')
->with(
$this->equalTo('usuario@ejemplo.com'),
$this->equalTo('Mensaje de prueba')
);
$controlador = new ControladorNotificaciones($emailServiceSpy);
$controlador->notificar('usuario@ejemplo.com', 'Mensaje de prueba');
}
}
El spy nos permite verificar que se cumplen las expectativas en cuanto a las interacciones, lo cual es esencial para asegurar el correcto funcionamiento del código.
Para evitar efectos colaterales, como escribir en el sistema de archivos, podemos usar un mock para simular la dependencia:
<?php
use PHPUnit\Framework\TestCase;
class GeneradorReporteTest extends TestCase
{
public function testGenerarReporte()
{
$archivoMock = $this->createMock(ArchivoInterface::class);
$archivoMock->expects($this->once())
->method('escribir')
->with('reporte.txt', 'Contenido del reporte');
$generador = new GeneradorReporte($archivoMock);
$generador->generar();
$this->assertTrue($generador->exito());
}
}
De esta forma, el test no altera el sistema de archivos, manteniendo el entorno de pruebas limpio.
Para mejorar la cobertura de las pruebas, podemos utilizar datos parametrizados. PHPUnit permite usar proveedores de datos:
<?php
use PHPUnit\Framework\TestCase;
class ValidadorEmailTest extends TestCase
{
/**
* @dataProvider proveedorEmailsValidos
*/
public function testEmailValido($email)
{
$validador = new ValidadorEmail();
$this->assertTrue($validador->esValido($email));
}
public function proveedorEmailsValidos()
{
return [
['usuario@dominio.com'],
['nombre.apellido@dominio.es'],
['correo+alias@ejemplo.org'],
];
}
}
Este enfoque facilita la creación de tests eficientes y mantenibles.
Para ejecutar las pruebas, podemos utilizar el servidor web integrado de PHP y gestionar dependencias con Composer. En el composer.json
:
{
"require-dev": {
"phpunit/phpunit": "^10.0"
}
}
Instalamos PHPUnit:
composer install
Iniciamos el servidor integrado:
php -S localhost:8000
Ejecutamos las pruebas:
vendor/bin/phpunit --color=auto tests
Este flujo garantiza un entorno de pruebas consistente y controlado.
Todas las lecciones de PHP
Accede a todas las lecciones de PHP y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.
Introducción A Php
Introducción Y Entorno
Instalación Y Primer Programa De Php
Introducción Y Entorno
Tipos De Datos, Variables Y Constantes
Sintaxis
Operadores Y Expresiones
Sintaxis
Estructuras De Control
Sintaxis
Funciones Y Llamada De Funciones
Sintaxis
Cadenas De Texto Y Manipulación
Sintaxis
Manejo De Números
Sintaxis
Manejo De Fechas Y Tiempo
Sintaxis
Manejo De Arrays
Sintaxis
Introducción A La Poo En Php
Programación Orientada A Objetos
Clases Y Objetos
Programación Orientada A Objetos
Constructores Y Destructores
Programación Orientada A Objetos
Herencia
Programación Orientada A Objetos
Encapsulación
Programación Orientada A Objetos
Polimorfismo
Programación Orientada A Objetos
Interfaces
Programación Orientada A Objetos
Traits
Programación Orientada A Objetos
Namespaces
Programación Orientada A Objetos
Autoloading De Clases
Programación Orientada A Objetos
Manejo De Errores Y Excepciones
Programación Orientada A Objetos
Manejo De Archivos
Programación Orientada A Objetos
Patrones De Diseño
Programación Orientada A Objetos
Introducción A Los Formularios En Php
Formularios
Procesamiento De Datos De Formularios
Formularios
Manejo De Archivos En Formularios
Formularios
Redirecciones Y Retroalimentación Al Usuario
Formularios
Formularios Dinámicos Y Separación De Lógica
Formularios
Introducción A La Persistencia En Php
Persistencia
Conexión A Bases De Datos
Persistencia
Consultas Y Operaciones Crud
Persistencia
Gestión De Transacciones
Persistencia
Manejo De Errores Y Excepciones En Base De Datos
Persistencia
Patrones De Acceso A Datos
Persistencia
Concepto De Sesiones En Php
Sesiones Y Cookies
Configuración De Sesiones
Sesiones Y Cookies
Cookies
Sesiones Y Cookies
Manejo Avanzado De Sesiones Y Cookies
Sesiones Y Cookies
Principales Vulnerabilidades En Php
Seguridad
Seguridad En Formularios Y Entrada De Datos
Seguridad
Protección Frente A Inyección Sql
Seguridad
Gestión De Contraseñas Y Cifrado
Seguridad
Seguridad En Sesiones Y Cookies
Seguridad
Configuraciones De Php Para Seguridad
Seguridad
Introducción Al Testing En Php
Testing
Phpunit
Testing
Cobertura De Código En Testing
Testing
Test Doubles (Mocks, Stubs, Fakes, Spies)
Testing
Pruebas De Integración Y Funcionales
Testing
En esta lección
Objetivos de aprendizaje de esta lección
- Comprender el uso y propósito de dobles de prueba en PHP.
- Distinguir entre mocks, stubs, fakes y spies.
- Implementar dobles de prueba utilizando PHPUnit.
- Mejorar la testabilidad del código mediante la inyección de dependencias.
- Aplicar principios de buen diseño, como la inversión de dependencias.