Test doubles (Mocks, Stubs, Fakes, Spies)

Experto
PHP
PHP
Actualizado: 17/02/2025

¡Desbloquea el curso completo!

IA
Ejercicios
Certificado
Entrar

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.

¿Te está gustando esta lección?

Inicia sesión para no perder tu progreso y accede a miles de tutoriales, ejercicios prácticos y nuestro asistente de IA.

Progreso guardado
Asistente IA
Ejercicios
Iniciar sesión gratis

Más de 25.000 desarrolladores ya confían en CertiDevs

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.

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

Completa PHP y certifícate

Únete a nuestra plataforma y accede a miles de tutoriales, ejercicios prácticos, proyectos reales y nuestro asistente de IA personalizado para acelerar tu aprendizaje.

Asistente IA

Resuelve dudas al instante

Ejercicios

Practica con proyectos reales

Certificados

Valida tus conocimientos

Más de 25.000 desarrolladores ya se han certificado con CertiDevs

⭐⭐⭐⭐⭐
4.9/5 valoración