PHPUnit en PHP: escribir y ejecutar tests unitarios, aserciones y mocks

PHPUnit es el framework de testing más usado en PHP. Permite escribir tests unitarios que verifican el comportamiento de clases y funciones de forma automática y repetible. Instalar PHPUnit, estructura los tests como clases que extienden TestCase, y ofrece aserciones, fixtures con setUp/tearDown, data providers y mocks para aislar dependencias externas.

Instalación y estructura básica

<?php
// Instalar como dependencia de desarrollo:
// composer require --dev phpunit/phpunit

// Estructura de directorios recomendada:
// src/
//   Calculadora.php
// tests/
//   CalculadoraTest.php
// phpunit.xml

// Clase a testear: src/Calculadora.php
namespace App;

class Calculadora
{
    public function sumar(float $a, float $b): float
    {
        return $a + $b;
    }

    public function dividir(float $a, float $b): float
    {
        if ($b == 0.0) {
            throw new InvalidArgumentException('No se puede dividir por cero.');
        }
        return $a / $b;
    }
}
?>

TestCase y aserciones básicas

<?php
// tests/CalculadoraTest.php
namespace Tests;

use AppCalculadora;
use PHPUnitFrameworkTestCase;

class CalculadoraTest extends TestCase
{
    private Calculadora $calc;

    protected function setUp(): void
    {
        $this->calc = new Calculadora();
    }

    public function testSumarDosEnteros(): void
    {
        $resultado = $this->calc->sumar(2, 3);
        $this->assertEquals(5, $resultado);
    }

    public function testSumarNegativo(): void
    {
        $this->assertEquals(-1, $this->calc->sumar(2, -3));
    }

    public function testDividirNormal(): void
    {
        $this->assertSame(2.5, $this->calc->dividir(5, 2));
    }

    public function testDividirPorCeroLanzaExcepcion(): void
    {
        $this->expectException(InvalidArgumentException::class);
        $this->expectExceptionMessage('No se puede dividir por cero.');
        $this->calc->dividir(10, 0);
    }
}
?>

Aserciones más usadas

<?php
use PHPUnitFrameworkTestCase;

class EjemploAserciones extends TestCase
{
    public function testAserciones(): void
    {
        // Igualdad (con conversión de tipo)
        $this->assertEquals(5, '5');        // pasa
        // Identidad estricta (tipo y valor)
        $this->assertSame(5, 5);            // pasa
        // $this->assertSame(5, '5');       // falla

        // Booleanos
        $this->assertTrue(1 === 1);
        $this->assertFalse(1 === 2);

        // Nulo
        $this->assertNull(null);
        $this->assertNotNull('valor');

        // Arrays
        $this->assertCount(3, [1, 2, 3]);
        $this->assertContains('php', ['python', 'php', 'js']);
        $this->assertArrayHasKey('nombre', ['nombre' => 'Ana']);

        // Strings
        $this->assertStringContainsString('php', 'manual de php');
        $this->assertMatchesRegularExpression('/^d{4}$/', '2024');

        // Tipos
        $this->assertIsArray([]);
        $this->assertIsString('texto');
        $this->assertInstanceOf(DateTime::class, new DateTime());
    }
}
?>

setUp(), tearDown() y data providers

<?php
namespace Tests;

use PHPUnitFrameworkAttributesDataProvider;
use PHPUnitFrameworkTestCase;

class OperacionesTest extends TestCase
{
    // Se ejecuta antes de CADA test
    protected function setUp(): void
    {
        // inicializar fixtures, conexión a BD de test, etc.
    }

    // Se ejecuta después de CADA test
    protected function tearDown(): void
    {
        // limpiar recursos, rollback de transacción, etc.
    }

    // Data provider: ejecuta el test con distintos conjuntos de datos
    public static function casosSuma(): array
    {
        return [
            'suma positivos'  => [2, 3, 5],
            'suma con cero'   => [0, 5, 5],
            'suma negativos'  => [-2, -3, -5],
            'suma mixta'      => [-1, 4, 3],
        ];
    }

    #[DataProvider('casosSuma')]
    public function testSumar(float $a, float $b, float $esperado): void
    {
        $calc = new AppCalculadora();
        $this->assertSame($esperado, $calc->sumar($a, $b));
    }
}
?>

Mocks con createMock()

<?php
namespace Tests;

use AppServicioRegistro;
use AppRepositoriosUsuarioRepositorioInterface;
use AppServiciosMailerInterface;
use AppServiciosLoggerInterface;
use PHPUnitFrameworkTestCase;

class ServicioRegistroTest extends TestCase
{
    public function testRegistrarUsuarioNuevo(): void
    {
        // Crear mocks de las dependencias
        $repo   = $this->createMock(UsuarioRepositorioInterface::class);
        $mailer = $this->createMock(MailerInterface::class);
        $logger = $this->createMock(LoggerInterface::class);

        // Configurar comportamiento del mock
        $repo->method('buscarPorEmail')->willReturn(null);     // email no existe
        $repo->method('crear')->willReturn(42);                 // devuelve ID 42
        $mailer->expects($this->once())->method('enviar');      // se llama exactamente 1 vez
        $logger->expects($this->once())->method('info');

        $servicio  = new ServicioRegistro($repo, $mailer, $logger);
        $idCreado  = $servicio->registrar('Ana', '[email protected]', 'pass123');

        $this->assertSame(42, $idCreado);
    }

    public function testRegistrarEmailDuplicadoLanzaExcepcion(): void
    {
        $repo = $this->createMock(UsuarioRepositorioInterface::class);
        $repo->method('buscarPorEmail')->willReturn(['id' => 1]); // email ya existe

        $servicio = new ServicioRegistro(
            $repo,
            $this->createMock(MailerInterface::class),
            $this->createMock(LoggerInterface::class),
        );

        $this->expectException(DomainException::class);
        $servicio->registrar('Ana', '[email protected]', 'pass123');
    }
}
?>

phpunit.xml: configuración básica

<?php
// phpunit.xml
// <?xml version="1.0"?>
// <phpunit bootstrap="vendor/autoload.php"
//          colors="true"
//          stopOnFailure="false">
//
//   <testsuites>
//     <testsuite name="Aplicación">
//       <directory>tests/</directory>
//     </testsuite>
//   </testsuites>
//
//   <source>
//     <include>
//       <directory>src/</directory>
//     </include>
//   </source>
// </phpunit>

// Ejecutar los tests:
// vendor/bin/phpunit
// vendor/bin/phpunit tests/CalculadoraTest.php
// vendor/bin/phpunit --filter testSumar
// vendor/bin/phpunit --coverage-html coverage/
?>

La documentación oficial de PHPUnit incluye la lista completa de aserciones, el uso de test doubles (stubs, spies, fakes), integración con Xdebug para cobertura de código y la configuración de entornos de CI para ejecutar los tests automáticamente en cada push.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP