Primeros pasos en phpUnit

A que adivino: apareció un bug en producción justo cuando estabas por irte a casa.

Viendo como el plan del fin de semana se aleja en el horizonte pensaste: basta. Es hora de tener tests automatizados.

Ok, tal vez la historia real no sea exactamente esta, pero apuesto a que estuve cerca.

Pues bien, el primer paso obligado es instalar phpUnit.

Esta parte es fácil:

composer require --dev phpunit/phpunit

Ojo con esto, verificá que la versión de phpunit sea compatible con tu versión de php.

Para este post asumiré que estás trabajando sobre php 8.3, con lo cual, la versión de phpunit que te corresponde es la 11. Es decir que el comando correcto sería:

composer require --dev phpunit/phpunit ^11

Antes de continuar, verificá que está todo en su lugar con:

./vendor/bin/phpunit --version

Si ves algo como:

PHPUnit 11.0.0 by Sebastian Bergmann and contributors.

Está todo listo para el siguiente paso: escribir un test.

Acá hay unas cuantas posibilidades dependiendo de la naturaleza de tu aplicación pero, para dejar este post de un tamaño razonable voy a hacer unas cuantas suposiciones (Si querés explorar algún caso más particular dejame un comentario).

Vamos a imaginar que querés testear una clase llamada EmailValidator que, oh sorpresa, se encarga de determinar si un string constituye o no, un correo válido.

Voy a poner el foco en el test, con lo cual, no me importa en este momento si EmailValidator hace lo que tiene que hacer o no, lo que me importa es poder escrbir un test que me permita verificarlo en todo momento.

Tu primer test con phpUnit

Como dije antes, voy a hacer unas cuantas suposiciones. Una de ellas es que tu proyecto está estructurado de un modo similar a:

/src
--/EmailValidator.php
/tests
composer.json

Y que el archivo composer.json define algún tipo de autoloading, con lo cual, un archivo tests/EmailValidatorTest.php que se va así debería funcionar:

<?php

declare(strict_types=1);

use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\Test;

class EmailValidatorTest extends TestCase
{
    #[Test]
    public function do_something(): void {
        $this->assertTrue(true);
    }
}

Este primer test, como te habrás dado cuenta, es tautológico, no te preocupes, no lo vamos a dejar así, es un paso temporal para validar que el framework de testing está bien configurado.

Ejecutalo con:

./vendor/bin/phpunit tests

Y si ves algo como

PHPUnit 11.4.2 by Sebastian Bergmann and contributors.

Runtime:       PHP 8.4.0RC1

.                                                                   1 / 1 (100%)

Time: 00:00.003, Memory: 6.00 MB

OK (1 test, 1 assertion)

Está todo en orden y podés pasar a la lógica de la verificación.

Ahora bien… ¿qué debería validar el test? Bueno… esto puede ponerse algo filosófico, por ahora lo dejo bien concreto: lo que debe verificar es que EmailValidator responde «sí» (o true) cuando se le pide validar un email y «no» (o false) cuando el parámetro es un string no válido como email.

En otras palabras: el test debe asegurar, con un umbral razonable de confianza, que la clase EmailValidator es capaz de reconocer direcciones de correo electrónico o, tal vez, es capaz de determinar si un string es o no un correo electrónico.

En todo caso, el test podría escribirse de este modo:

<?php

declare(strict_types=1);

use App\EmailValidator;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\Test;

class EmailValidatorTest extends TestCase
{
    #[Test]
    public function should_determine_whether_a_string_is_a_valid_email(): void {
        $validator = new EmailValidator();
        $this->assertTrue($validator->isValid("mauro.chojrin@leewayweb.com"));
    }
}

Y, al ejecutarlo, lo esperable es que pase. Salvo, por supuesto, que el EmailValidator no esté correctamente implementado, algo que, para este ejemplo, asumiré que no es el caso.

Bien, con esto tenemos cubierto el caso positivo (Al menos uno de ellos), faltaría cubrir el caso negativo, es decir, que el EmailValidator sabe reconocer falsos correos.

Para eso podríamos hacer algo como:

<?php

declare(strict_types=1);

use App\EmailValidator;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\Test;

class EmailValidatorTest extends TestCase
{
    #[Test]
    public function should_determine_whether_a_string_is_a_valid_email(): void {
        $validator = new EmailValidator();
        $this->assertTrue($validator->isValid("mauro.chojrin@leewayweb.com"));
        $this->assertFalse($validator->isValid("mauro.chojrin.leewayweb.com"));
    }
}

No está necesariamente mal tener más de un assertion por test aunque, personalmente, preferiría separar los datos de la lógica del test.

Para eso, la recomendación es usar un test parametrizado:

<?php

declare(strict_types=1);

use App\EmailValidator;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\Test;

class EmailValidatorTest extends TestCase
{
    public static function dataProvider(): array
    {
        return [
            [ "mauro.chojrin@leewayweb.com", true ],
            [ "mauro.chojrin.leewayweb.com", false ],
        ];
    }

    #[Test]
    #[DataProvider("dataProvider")]
    public function should_determine_whether_a_string_is_a_valid_email(string $candidate, bool $isEmail): void {
        $validator = new EmailValidator();
        $this->assertEquals($isEmail, $validator->isValid($candidate));
    }
}

De esta forma, cuando quieras agregar casos de prueba bastará con modificar el método dataProvider, por ejemplo así:

    public static function dataProvider(): array
    {
        return [
            [ "mauro.chojrin@leewayweb.com", true ],
            [ "mauro.chojrin+1@leewayweb.com", true ],
            [ "mauro.chojrin.leewayweb.com", false ],
            [ "mauro.chojrin   @leewayweb.com", false ],
        ];
    }

Y así podés seguir construyendo más y más casos de prueba hasta que te sientas seguro de que la implementación de la clase cumple sus requerimientos.

Claro que hay mucho (¡mucho!) más para aprender pero bueno… por algún lado hay que empezar, ¿no?

Por si tenés problemas en el camino

Algunas cosas que pueden fallarte en el camino y que son simples de resolver:

Faltan extensiones de php

phpUnit requiere de estas extensiones:

  • ext-dom
  • ext-mbstring

Dependiendo de tu ambiente de trabajo es posible que no se encuentren disponibles apenas arrancás. Si este es el caso, las podrás instalar usando el manejador de paquetes de tu sistema operativo.

No se reconocen las clases productivas

Es posible que la primera vez que ejecutes tu código te encuentres con un problema como este:

PHPUnit 11.4.2 by Sebastian Bergmann and contributors.

Runtime:       PHP 8.4.0RC1
Configuration: /home/mauro/Code/phpunit101/phpunit.xml

EE                                                                  2 / 2 (100%)

Time: 00:00.006, Memory: 8.00 MB

There were 2 errors:

1) EmailValidatorTest::should_determine_whether_a_string_is_a_valid_email with data set #0 ('mauro.chojrin@leewayweb.com', true)
Error: Class "App\EmailValidator" not found

/home/mauro/Code/phpunit101/tests/EmailValidatorTest.php:23

2) EmailValidatorTest::should_determine_whether_a_string_is_a_valid_email with data set #1 ('mauro.chojrin.leewayweb.com', false)
Error: Class "App\EmailValidator" not found

/home/mauro/Code/phpunit101/tests/EmailValidatorTest.php:23

ERRORS!
Tests: 2, Assertions: 0, Errors: 2.

Si eso pasa, verificá cómo está configurado tu autoloading en tu archivo composer.json. Debería verse así:

{
  "autoload": {
    "psr-4": {
      "App\\": "src/"
    }
  }
  "require-dev": {
    "phpunit/phpunit": "^11"
  }
}

También es posible que necesites crear el archivo phpunit.xml en la raíz de tu proyecto con este contenido:

<phpunit
   bootstrap="vendor/autoload.php"
/>

Y para estar 100% seguro, no está de más ejecutar:

composer dump-autoload

Con esto deberías tener todo lo necesario.

Por dónde seguir

La documentación oficial de phpUnit siempre es una buena fuente.

Te recomiendo mirarte especialmente la parte de la configuración y los dobles de test.

¡A testear se ha dicho!

mchojrin
Publicada el
Categorizado como Ejemplos Etiquetado como

Por mchojrin

Ayudo a desarrolladores PHP a afinar sus habilidades técnicas y avanzar en sus carreras

¿Te quedó alguna duda? Publica aca tu pregunta

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.