PHP

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ícate

Uso 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.

Para seguir leyendo hazte Plus

¿Ya eres Plus? Accede a la app

20 % DE DESCUENTO

Plan mensual

19.00 /mes

15.20 € /mes

Precio normal mensual: 19 €
58 % DE DESCUENTO

Plan anual

10.00 /mes

8.00 € /mes

Ahorras 132 € al año
Precio normal anual: 120 €
Aprende PHP GRATIS online

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

PHP

Introducción Y Entorno

Instalación Y Primer Programa De Php

PHP

Introducción Y Entorno

Tipos De Datos, Variables Y Constantes

PHP

Sintaxis

Operadores Y Expresiones

PHP

Sintaxis

Estructuras De Control

PHP

Sintaxis

Funciones Y Llamada De Funciones

PHP

Sintaxis

Cadenas De Texto Y Manipulación

PHP

Sintaxis

Manejo De Números

PHP

Sintaxis

Manejo De Fechas Y Tiempo

PHP

Sintaxis

Manejo De Arrays

PHP

Sintaxis

Introducción A La Poo En Php

PHP

Programación Orientada A Objetos

Clases Y Objetos

PHP

Programación Orientada A Objetos

Constructores Y Destructores

PHP

Programación Orientada A Objetos

Herencia

PHP

Programación Orientada A Objetos

Encapsulación

PHP

Programación Orientada A Objetos

Polimorfismo

PHP

Programación Orientada A Objetos

Interfaces

PHP

Programación Orientada A Objetos

Traits

PHP

Programación Orientada A Objetos

Namespaces

PHP

Programación Orientada A Objetos

Autoloading De Clases

PHP

Programación Orientada A Objetos

Manejo De Errores Y Excepciones

PHP

Programación Orientada A Objetos

Manejo De Archivos

PHP

Programación Orientada A Objetos

Patrones De Diseño

PHP

Programación Orientada A Objetos

Introducción A Los Formularios En Php

PHP

Formularios

Procesamiento De Datos De Formularios

PHP

Formularios

Manejo De Archivos En Formularios

PHP

Formularios

Redirecciones Y Retroalimentación Al Usuario

PHP

Formularios

Formularios Dinámicos Y Separación De Lógica

PHP

Formularios

Introducción A La Persistencia En Php

PHP

Persistencia

Conexión A Bases De Datos

PHP

Persistencia

Consultas Y Operaciones Crud

PHP

Persistencia

Gestión De Transacciones

PHP

Persistencia

Manejo De Errores Y Excepciones En Base De Datos

PHP

Persistencia

Patrones De Acceso A Datos

PHP

Persistencia

Concepto De Sesiones En Php

PHP

Sesiones Y Cookies

Configuración De Sesiones

PHP

Sesiones Y Cookies

Cookies

PHP

Sesiones Y Cookies

Manejo Avanzado De Sesiones Y Cookies

PHP

Sesiones Y Cookies

Principales Vulnerabilidades En Php

PHP

Seguridad

Seguridad En Formularios Y Entrada De Datos

PHP

Seguridad

Protección Frente A Inyección Sql

PHP

Seguridad

Gestión De Contraseñas Y Cifrado

PHP

Seguridad

Seguridad En Sesiones Y Cookies

PHP

Seguridad

Configuraciones De Php Para Seguridad

PHP

Seguridad

Introducción Al Testing En Php

PHP

Testing

Phpunit

PHP

Testing

Cobertura De Código En Testing

PHP

Testing

Test Doubles (Mocks, Stubs, Fakes, Spies)

PHP

Testing

Pruebas De Integración Y Funcionales

PHP

Testing

Accede GRATIS a PHP y certifícate

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.