Etiqueta: Testing

  • Cómo usar PHPUnit

    Cómo usar PHPUnit

    Te decidiste. Llegó la hora de incorporar el testing automatizado a tus proyectos.

    Permitime que te felicite, es un gran paso hacia la generación de software de calidad superior.

    Después de hacer la debida investigación hay pocas dudas: PHPUnit es la herramienta que debes conocer.

    Para hacerte un poco más sencillo el camino, te dejo los lineamientos para dar tus primeros pasos.

    Cómo instalar PHPUnit

    Lo primero, como de costumbre, será instalar la herramienta.

    Si vas al sitio de phpunit.de encontrarás algo como:

    Nada mal para tener a mano pero, para empezar de cero… puede ser algo intimidante.

    Antes de continuar, una advertencia: las versiones de phpUnit están bastante ligadas a las de php. Esto significa que, para asegurarte de elegir una versión que puedas utilizar, debes saber qué versión de php tienes instalada (O piensas instalar, por ejemplo, si utilizarás docker).

    Seleccionar la versión de phpunit

    Asumamos que, para arrancar, usarás el php instalado en tu propio host.

    Un simple php -v alcanzará:

    Pues bien, en este caso, la versión que más conviene utilizar es la 11, como puede verse aquí:

    El siguiente paso: instalar la librería.

    Instalar phpunit mediante el phar

    La primera opción disponible es descargar el paquete cerrado (el archivo .phar) de aquí y hacerlo ejecutable.

    Esta opción puede resultar útil si quieres dejar una única versión de phpunit disponible para todos tus proyectos.

    Instalar phpunit usando composer

    Por lejos, la forma que recomiendo (y utilizo) es hacerlo a través de composer.

    De esta forma, la dependencia quedará circunscripta al proyecto en el que estás trabajando en este momento, a la vez que puedes compartir esta configuración con el resto de tu equipo.

    Usa composer require --dev phpunit/phpunit ^11 y composer se encargará de todo.

    Para verificar tu instalación usa el comando ./vendor/bin/phpunit --version

    Si ves algo como

    PHPUnit 11.4.4 by Sebastian Bergmann and contributors.

    PHPUnit 11.4.4 by Sebastian Bergmann and contributors.

    Estás listo para avanzar.

    Cómo escribir un test con PHPUnit

    Escribir un test de PHPUnit supone crear una nueva clase que extienda de TestCase:

    <?php declare(strict_types=1);
    use PHPUnit\Framework\TestCase;
    
    final class MyTest extends TestCase
    {
    }

    Para ejecutar tus tests puedes utilizar el comando ./vendor/bin/phpunit MyTest.php y obtendrás una salida como:

    PHPUnit 11.4.4 by Sebastian Bergmann and contributors.
    
    Runtime:       PHP 8.4.1
    
    There was 1 PHPUnit test runner warning:
    
    1) No tests found in class "MyTest".
    
    No tests executed!

    Bastante razonable ¿no? Al fin y al cabo, se ha definido un TestCase pero ningún test dentro. Corrijamos eso.

    Por convención, PHPUnit entenderá que cualquier método de una clase TestCase cuyo nombre comience por test debe ser ejecutado por él, es decir, lo próximo que deberías hacer es agregar un método como este:

    <?php declare(strict_types=1);
    use PHPUnit\Framework\TestCase;
    
    final class MyTest extends TestCase
    {
       public function testSomething(): void
       {
       }
    }

    Al ejecutar este test verás algo como:

    PHPUnit 11.4.4 by Sebastian Bergmann and contributors.
    
    Runtime:       PHP 8.4.1
    
    R                                                                   1 / 1 (100%)
    
    Time: 00:00.003, Memory: 8.00 MB
    
    There was 1 risky test:
    
    1) MyTest::testSomething
    This test did not perform any assertions
    
    /home/mauro/phpunit-poc/MyTest.php:6
    
    OK, but there were issues!
    Tests: 1, Assertions: 0, Risky: 1.

    Es decir, se ha ejecutado un test, sólo que este test no ha verificado nada, es decir, no hay realizado ningún assertion.

    Para que efectivamente aporte algo de información, dentro del cuerpo del test debe utilizarse alguno de los métodos assert* que provee phpUnit, por ejemplo:

    <?php declare(strict_types=1);
    use PHPUnit\Framework\TestCase;
    
    final class MyTest extends TestCase
    {
       public function testSomething(): void
       {
          $this->assertTrue(true);
       }
    }

    Y ahora sí, llegarás a una salida algo más parecida a lo deseable:

    PHPUnit 11.4.4 by Sebastian Bergmann and contributors.
    
    Runtime:       PHP 8.4.1
    
    .                                                                   1 / 1 (100%)
    
    Time: 00:00.003, Memory: 8.00 MB
    
    OK (1 test, 1 assertion)

    Por supuesto que este test en la realidad no aporta mucho. Depende de vos hacer tests que sean relevantes para tu aplicación.

    Ejemplo de un test con PHPUnit

    Veamos un ejemplo algo más realista de lo que se puede hacer con phpUnit.

    Imaginemos que tienes una función que, recibe una lista de números de teléfono y una lista de prefijos y retorna aquellos que comienzan por alguno de los prefijos indicados por el segundo argumento.

    Un test para dicha función podría ser este:

    <?php declare(strict_types=1);
    use PHPUnit\Framework\TestCase;
    
    final class MyTest extends TestCase
    {
            public function testPhoneFilter(): void
            {
                    $filteredPhoneNumbers = filter_phone_numbers(
                            [ '(34) 665-55-22-112', '34 992 11 22 33', '+54 9 11 5494 2211', '054 121 123123' ],
                            [ '34', '054' ],
                    );
    
                    $this->assertEquals( [ '34 992 11 22 33', '054 121 123123'] , $filteredPhoneNumbers );
            }
    }

    Por dónde continuar aprendiendo

    PHPUnit ofrece una cantidad de posibilidades realmente interesante pero, para no marearte con todo al comienzo te recomiendo continuar aprendiendo sobre dobles de test y sobre la configuración de phpUnit.

    Ya habrá tiempo para lo demás.

  • Cómo testear una aplicación que depende de una API sin conexión a Internet

    Venís tirando código como un campeón.

    Las integraciones con las APIs de Twitter y Google no esconden ningún secreto.

    Sólo te falta probar el último feature.

    Le das a correr y…

    Timeout.

    Lo qué??

    Revisás el código de arriba abajo… ¿Cómo es posible?

    Si hace 5 minutos funcionaba de 10…

    Levantás la vista y ves como el router te sonríe con sorna:

    Como diciéndote: A ver cuándo vas a aprender quién manda acá.

    ¿Otra vez se cayó Internet?

    ¿De nuevo vas a tener que llamar al proveedor para que lo reparen de una buena vez?

    Y… otra no queda… si hay que testear la API… se necesita conectividad…

    ¿O no?

    Y de pronto… muy bajito en la distancia se escucha la voz del maestro Yoda susurrando «Tests escribir debes».

    Exacto. Si querés tener un entorno sólido de desarrollo tenés que aislarte de las dependencias de terceros.

    Cómo escribir tests que no dependan de terceros

    Las soluciones son múltiples y mucho dependerá del contexto pero todas siguen la misma lógica: en vez de depender de un servicio poco confiable (O al menos, uno que está fuera de tu control), usá un doble del cual tengas control total.

    Usar un doble de test a nivel unitario

    Empecemos por un ejemplo simple a nivel de test unitario.

    Imaginemos que tenés un código que usa una librería como esta y es algo del estilo de:

    <?php
    
    declare(strict_types=1);
    
    namespace App;
    use Exception;
    use GuzzleHttpExceptionGuzzleException;
    use NowehTwitterApiClient;
    
    readonly class TwitterMonitor
    {
        public function __construct(private Client $client)
        {
        }
    
        /**
         * @return array<Mention>
         * @throws Exception
         * @throws GuzzleException
         */
        public function getRecentMentions(string $uid): array
        {
            $mentions = $this->client
                ->timeline()
                ->getRecentMentions($uid)
                ->performRequest()
            ;
    
            foreach ($mentions->data as $mention) {
                $return[] = new Mention($mention);
            }
    
            return $return;
        }
    }

    Está claro que, si $client->performRequest() falla, por ejemplo por falta de conectividad, todo lo que sigue va a fallar.

    Algo que podrías hacer en tu test es usar un mock. Sería algo así como:

    <?php
    
    declare(strict_types=1);
    
    require_once __DIR__ . '/../vendor/autoload.php';
    
    use App\Mention;
    use App\TwitterMonitor;
    use GuzzleHttp\Exception\GuzzleException;
    use Noweh\TwitterApi\Timeline;
    use PHPUnit\Framework\Attributes\Test;
    use PHPUnit\Framework\TestCase;
    use Noweh\TwitterApi\Client;
    
    class TwitterMonitorShould extends TestCase
    {
        const string UID = "1346889436626259968";
    
        private static function a2stdClass(array $array): stdClass
        {
            return json_decode(json_encode($array));
        }
    
        /**
         * @throws Exception|GuzzleException
         */
        #[Test]
        public function returnUsersLastMentions(): void
        {
            $timeline = $this
                ->getMockBuilder(Timeline::class)
                ->disableOriginalConstructor()
                ->getMock();
    
            $timeline
                ->expects($this->once())
                ->method('getRecentMentions')
                ->with(self::UID)
                ->willReturn($timeline);
    
            $mentionsData = self::a2stdClass([
                'data' => [
                    [
                        "author_id" => "2244994945",
                        "created_at" => "Wed Jan 06 18:40:40 +0000 2021",
                        "id" => "1346889436626259968",
                        "text" => "Learn how to use the user Tweet timeline and user mention timeline endpoints in the X API v2 to explore Tweet\\u2026 https:\\/\\/t.co\\/56a0vZUx7i",
                        "username" => "XDevelopers"
                    ]
                ]
            ]);
            $timeline
                ->expects($this->once())
                ->method('performRequest')
                ->willReturn($mentionsData);
    
            $client = $this
                ->getMockBuilder(Client::class)
                ->disableOriginalConstructor()
                ->getMock();
    
            $client
                ->expects($this->once())
                ->method('timeline')
                ->willReturn($timeline);
    
            $userMonitor = new TwitterMonitor($client);
    
            $this->assertEquals([new Mention($mentionsData->data[0]),], $userMonitor->getRecentMentions(self::UID));
        }
    }

    De esta forma, cuando ejecutes tus tests con phpUnit la llamada a Twitter no se realizará, si no que será el doble quien responda con la respuesta pre-armada.

    Usar un servicio mockeado

    Esta técnica puede ser algo más compleja de implementar pero probablemente sea mejor si lo que buscás es hacer algún tipo de test de caja negra.

    La idea aquí es «engañar» a tu aplicación para que crea que está hablando con el servicio real cuando, en realidad, está interactuando con uno local.

    Si no querés montarte tu propio servicio, podés usar una herramienta como mockoon, la idea es que puedas hacerle peticiones como si estuvieras yendo contra el servicio real.

    Es importante comprender que lo que podés testear es aquello que controlás, para todo lo demás tenés que asumir que el comportamiento esperado es el real. En otras palabras: dado que los terceros hacen su parte como se espera, tu aplicación debe comportarse como se espera, en caso de que no se cumpla la premisa no estás obligado a dar garantías.

    Obviamente, te conviene dar garantías de que tu aplicación nunca va a generar daños pero esa es otra historia.

    Veamos cómo sería en este caso.

    Ante todo, tenemos que averiguar a qué URL estamos haciendo los requests. Esta parte no es muy compleja: se trata de seguir la cadena de llamadas hacia atrás:

    Empezamos por Client::timeline() donde nos encontramos con que la respuesta es un objeto Timeline, el cual será el que eventualmente ejecutará el método performRequest.

    Entramos un poco más y nos encontramos con:

    if ($this->auth_mode === 0) { // Bearer Token
                    // Inject the Bearer token header
                    $client = new Client(['base_uri' => self::API_BASE_URI]);
                    $headers['Authorization'] = 'Bearer ' . $this->bearer_token;
                } elseif ($this->auth_mode === 1) { // OAuth 1.0a User Context
                    // Insert Oauth1 middleware
                    $stack = HandlerStack::create();
                    $middleware = new Oauth1([
                        'consumer_key' => $this->consumer_key,
                        'consumer_secret' => $this->consumer_secret,
                        'token' => $this->access_token,
                        'token_secret' => $this->access_token_secret,
                    ]);
                    $stack->push($middleware);
                    $client = new Client([
                        'base_uri' => self::API_BASE_URI,
                        'handler' => $stack,
                        'auth' => 'oauth'
                    ]);
                } else { // OAuth 2.0 Authorization Code Flow
                    throw new RuntimeException('OAuth 2.0 Authorization Code Flow had not been implemented & also requires user interaction.');
                }

    Es decir: la pieza clave aquí es la constante AbstractController::API_BASE_URI, cuya definición no es otra que https://api.twitter.com/2/… qué sorpresa, ¿no?

    Vamos a usar este dato para configurar nuestro proxy de modo de que la aplicación jamás se entere de que está hablando con un doble y no con el verdadero Twitter.

    Descargar la especificación de OpenAPI de la documentación de Twitter es un buen punto de partida.

    El siguiente paso es importarla en Mockoon:

    Con esto estarás listo para tener un servidor mockeado para empezar a jugar

    Por ejemplo, podrías cambiar el contenido de la respuesta que generó Mockoon para que retorne:

    "data": [
        {
          "author_id": "22123122345",
          "created_at": "Wed Oct 23 18:10:20 +0000 2024",
          "id": "2346889456626258962",
          "text": "Check out this article by @mchojrin!",
          "username": "PHP4Ever"
        }
      ],

    Y probar tu aplicación vía CLI o web, depende del caso a ver si está levantando esta respuesta.

    Bueno… en realidad todavía vas a tener un pequeño inconveniente en este caso en particular: la librería tiene la URL de Twitter hardcodeada y vos necesitás que Mockoon atrape todos esos requests.

    Hay varias soluciones que podés tomar para esto, las nombro solamente para no irme demasiado del tema de este post, en todo caso, escribiré algún otro detallando:

    • Cambiar el código de la librería para que apunte, en lugar de a twitter.com a localhost:3000 (La mala)
    • Montar un proxy en tu computadora que redirija el tráfico destinado a twitter.com hacia localhost:3000 (La fea)
    • Derivar una clase de la que te propone la librería y hacer que la URL base sea un parámetro del constructor (La buena)

    Más allá de cuál sea la solución que tomes, con este esquema vas a poder testear End-to-End sin depender de la conectividad.

    El lado oscuro de los dobles

    Si bien estas técnicas son muy útiles para trabajar tranquilo, dependen de un hecho fundamental: las respuestas hechas a mano deben coincidir (en estructura al menos) con aquellas devueltas por el servicio real.

    Si bien esto puede parecer obvio, es importante recordarlo porque muchas veces la propia documentación de la API contra la que estás trabajando está desactualizada y, en ese caso… nada más cierto que la realidad misma.

  • 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!

  • Cómo escribir pruebas unitarias para valores aleatorios en php

    Cómo escribir pruebas unitarias para valores aleatorios en php

    Te decidiste: llegó el momento de implementar phpUnit en proyecto. Emocionante, ¿no? Por fin vas a poder considerarte un desarrollador profesional.

    Ya instalaste las herramientas, te leiste unos cuantos tutoriales… todo listo.

    Los primeros tests no fueron tan complicados. Llegar al esperado verde costó un poco al comienzo pero lo sacaste.

    Y justo cuando la cosa se ponía interesante, te encontrás con:

    <?php
    
    class MyClass
    {
        public function testableMethod(int $param) : int
        {
            if (rand(1, 100) < 50) {
                return $param * 2;       
            } else {
                return $param * 3;
            }
        }
    }

    ¿Y ahora?

    ¿Cómo verificar que el método hace lo que tiene que hacer si el resultado depende de algo que no podés controlar?

    Está claro que TDD no funcionará para este caso… mejor dejar todo esto atrás y seguir trabajando como hasta ahora… al fin y al cabo, tan mal no te ha ido, ¿cierto?

    Si estás pensando esto, te propongo que sigas leyendo. Hay una salida a este dilema.

    La solución se basa en una característica muy popular, aunque muchas veces no muy comprendida, de la Programación Orientada a Objetos: la herencia.

    ¿Cómo? ¿Qué tiene que ver la herencia en todo esto? Veámoslo.

    Preparando el código PHP para las pruebas unitarias

    Tu primera aproximación a un test probablemente se verá similar a:

    <?php
    
    use PHPUnit\Framework\TestCase;
    
    use MyClass;
    
    class MyClassTest extends TestCase
    {
        public function testTestableMethod()
        {
            $sut = new MyClass();
            $this->assertEquals(..., $sut->testableMethod(5));
        }
    }

    El problema es, precisamente, con qué rellenar los .... Pues bien, ahí es donde viene la magia.

    ¿Qué tal si en lugar de testar directamente MyClass usaras una instancia de algo muy parecido a MyClass? Por ejemplo, una clase derivada de MyClass.

    Vamos por partes mejor.

    Comencemos por hacer a testableMethod un poco más test-friendly.

    Algo muy sencillo (y 100% seguro) de hacer es aislar la parte del método testableMethod que está impidiendo hacer el test.

    En este caso, el problema es la obtención del número aleatorio (rand(1, 100)), así que, el primer paso sería hacer una nueva versión del método testableMethod para que se vea así:

    public function testableMethod(int $param) : int
    {
        if ($this->getRandomNumber() < 50) {
            return $param * 2;
        } else {
            return $param * 3;
        }
    }

    Y el nuevo método getRandomNumber se vería así:

    protected function getRandomNumber(): int
    {
        return rand(1, 100);
    }

    Es claro que a nivel funcional no ha cambiado nada. Sin embargo, como veremos pronto, esta sutil diferencia es crucial para el desarrollo de la prueba unitaria.

    Un punto sumamente interesante es que este cambio está libre de riesgo en tanto que puede ser realizado en forma automática por un IDE.

    Continuemos entonces con el test.

    ¿Qué tal si creamos una nueva clase TestableMyClass que tenga exactamente el mismo comportamiento que MyClass, salvo por la forma de responder a getRandomNumber?

    class TestableMyClass extends MyClass
    {
        private int $randomNumber;
    
        /**
         * @param int $randomNumber
         */
        public function __construct(int $randomNumber) 
        {
            $this->randomNumber = $randomNumber;
        }
    
        /**
         * @return int
         */
        protected function getRandomNumber(): int
        {
            return $this->randomNumber;
        }
    }

    De esta forma tenemos una nueva implementación de MyClass que, en lugar de retornar números aleatorios, retornará un valor que nosotros controlamos y, de esa forma, podremos realizar el test que buscamos:

    class MyClassTest extends TestCase
    {
        /**
         * @dataProvider randomNumberProvider
         * @param int $baseNumber
         * @param int $multiplier
         * @param int $randomNumber
         * @return void
         */
        public function testTestableMethod(int $baseNumber, int $multiplier, int $randomNumber): void
        {
            $sut = new TestableMyClass($randomNumber);
    
            $this->assertEquals($baseNumber * $multiplier, $sut->testableMethod($baseNumber));
        }
    
        /**
         * @return int[][]
         */
        public function randomNumberProvider(): array
        {
            return [
                [ 1, 2, 2 ],
                [ 20, 2, 40 ],
                [ 51, 3, 153 ],
                [ 60, 3, 180 ],
            ];
        }
    }

    Y voilà! Tenemos un test que garantiza la correctitud del método.

    De hecho, para hacerlo todavía más interesante podríamos usar una clase que genere números aleatorios e inyectarla como colaborador de MyClass pero bueno… tema para otro post.

  • Cómo mejorar la calidad de tus aplicaciones PHP

    Cómo mejorar la calidad de tus aplicaciones PHP

    A nadie le gusta encontrar bugs en sus aplicaciones.

    Mucho menos que sea el cliente quien los encuentre.

    ¿Qué podemos hacer para evitar estas desagradables situaciones? Testear.

    Qué significa testear una aplicación

    Testear una aplicación puede significar varias cosas pero básicamente, se trata de probar el funcionamiento tratando de confirmar que todo ocurre según lo planeado.

    La parte desagradable del testing es que sólo puede darnos seguridad de que la aplicación falla.

    En otras palabras el hecho de que todas las pruebas resulten exitosas puede significar dos cosas:

    1. Que efectivamente la aplicación esté libre de errores
    2. Que no hayamos realizado suficientes pruebas

    Desafortunadamente no es posible determinar si estamos en la situación uno o la dos.

    ¡Claro que esto no significa que testear no valga la pena!

    El punto es que nunca podremos tener un 100% de confianza en que la aplicación es correcta… aunque tener algo de confianza es mejor que no tener nada.

    Qué tipos de testing existen

    Existen diversos tipos de pruebas que pueden realizarse en aplicaciones.

    La más básica es el testing manual.

    Este tipo de prueba se trata de uno mismo usando la aplicación como espera que lo haría un usuario y evaluando los resultados a ojo.

    Claramente se trata del testing más simple que puede realizarse pero, a su vez, el menos ventajoso:

    • Es lento
    • Es tedioso
    • Es poco confiable

    Una segunda categoría de tests son los automatizados y, dentro de ellos, existen subdivisiones:

    • Los tests unitarios
    • Los tests de integración
    • Los tests de aceptación

    Qué son los tests unitarios

    Los tests unitarios son aquellos en los que se prueba una parte muy específica del código (Un método por ejemplo).

    Se sabe de antemano qué resultado debe retornar una función dado un input y se verifica que así suceda.

    Qué son los tests de integración

    Los tests de integración son similares a los unitarios, salvo que lo que se prueba no es una unidad en aislamiento si no cómo dos o más unidades interactúan entre sí.

    Qué son los tests de aceptación

    Los tests de aceptación apuntan a verificar el correcto funcionamiento del sistema como tal, sin tomar en cuenta el código que está detrás.

    La idea de estas pruebas es automatizar las que se realizarían en forma manual.

    Qué herramientas pueden usarse para testear PHP

    Existen varias herramientas que pueden utilizarse para verificar el código escrito con PHP.

    La más conocida y utilizada es phpUnit.

    Si bien esta herramienta está diseñada para la realización de pruebas unitarias, es posible utilizarla en otros tipos de pruebas también.

    Las pruebas de aceptación requieren algún mecanismo de automatización de la interface de usuario (En el caso de aplicaciones web se trata de automatizar el navegador).

    Una herramienta muy interesante para realizar pruebas de aceptación es CodeCeption para escribir las pruebas y Selenium para automatizar el uso del navegador.

    ¿Qué partes del código deben ser testeadas?

    Como te podrás imaginar, no es posible tener el 100% del código cubierto por tests… una aplicación de mediana complejidad plantea un número realmente elevado de escenarios.

    Esto quiere decir que debemos elegir qué partes deben ser verificadas y cuáles no.

    La decisión no es sencilla, pero hay que tratar de fallar en favor de las partes críticas del sistema (Aquellas que, de no funcionar correctamente nos pondrían en serios problemas).

    La buena noticia es que los tests, al ser automatizados, pueden ejecutarse tantas veces como sea necesario y, a medida que se descubren nuevos inconvenientes, nuevos tests pueden ser agregados al conjunto, haciendo que la aplicación se robustezca más y más.

    ¿Lo único que puede hacerse es testear?

    No.

    Existe otra serie de herramientas que apuntan a detectar problemas antes de ejecutar el código.

    En lenguajes compilados por ejemplo, la verificación de compatibilidad de tipos de datos se produce antes de la ejecución de los programas… no es el caso de PHP.

    Herramientas como phpStan pueden ayudar a mejorar la calidad del código mientras se está desarrollando.

    Conclusión

    El testing es una práctica sumamente importante a la hora de realizar desarrollos profesionales.

    ¿De qué forma verificas la calidad de tus aplicaciones?

  • Cómo testear una aplicación PHP que no usa objetos

    Cómo testear una aplicación PHP que no usa objetos

    PHPUnit, al igual que la mayoría de los frameworks de testing, se basa fuertemente en el supuesto de que la aplicación a verificar está desarrollada bajo el paradigma de Orientación a Objetos.

    Sin embargo, es muy común en nuestros días encontrarnos con aplicaciones tipo spaghetti… ¿es posible hacer testing automatizado sobre ellas?

    La respuesta es sí.

    Claro que las respuestas a qué testear y cómo testear son un poco diferentes.

    Qué puede testearse en una aplicación que no usa objetos

    Obviamente, no será posible verificar una clase porque… la aplicación no tiene clases.

    De modo que podemos testear:

    • La página que se presentará al usuario (Lo que podríamos asemejar a un test funcional)
    • El resultado de ejecutar alguna función en particular
    • El resultado de correr algún script

    Cómo testear el resultado de una página php

    Para este escenario nos tendremos que valer de un pequeño truco: las funciones ob_start y ob_get_clean (Además de tener instalado phpUnit, claro).

    La idea es muy simple en realidad.

    Se crea un caso de test, se abre un buffer y se incluye el archivo que queremos validar.

    A continuación se levantan los contenidos del buffer y se examinan usando assertions.

    Veamos un ejemplo simple:

    El archivo que queremos validar se llama wrong.php

    <html>
    <body>
    <p><?php echo 'Bye bye world'; ?></p>
    </body>
    </html>

    Y este sería el caso de test:

    <?php
    
    use PHPUnit\Framework\TestCase;
    
    class PageTest extends TestCase
    {
            public function testGreeting()
            {
                    ob_start();
                    require_once 'wrong.php';
                    $this->assertRegExp('/<p>Hello World!<\/p>/', ob_get_clean());
            }
    }

    Para correr el test usamos el comando vendor/bin/phpunit PageTest.php y la salida será:

    PHPUnit 9.3.7 by Sebastian Bergmann and contributors.
    
    F                                                                   1 / 1 (100%)
    
    Time: 00:00.004, Memory: 4.00 MB
    
    There was 1 failure:
    
    1) PageTest::testGreeting
    Failed asserting that '<html>\n
    <body>\n
    	<p>Bye bye world</p>\n
    </body>\n
    </html>\n
    ' matches PCRE pattern "/<p>Hello World!<\/p>/".
    
    /home/mauro/Code/testing/PageTest.php:11
    
    FAILURES!
    Tests: 1, Assertions: 1, Failures: 1.

    Cómo testear el resultado de una función php

    Este caso es bastante similar al anterior, aunque un poco más simple.

    Aquí lo que haremos será, en lugar de validar la salida completa, verificaremos qué sucedió como resultado de ejecutar la función.

    Empecemos por modificar el archivo a testear:

    <?php
    
    function duplicate(int $p) : int
    {
            return $p * 3;
    }

    Y ahora hagamos un nuevo test:

    <?php
    
    use PHPUnit\Framework\TestCase;
    
    class FunctionTest extends TestCase
    {
            public function testFunction()
            {
                    ob_start();
                    require_once 'wrong.php';
    
                    $this->assertEquals(4, duplicate(2));
            }
    }

    El resto sigue igual al caso anterior

    Cómo testear la ejecución de un script php

    Por último, podríamos requerir testear el funcionamiento de un script de línea de comandos (Un cronjob por ejemplo).

    Imaginemos un script como este:

    <?php
    
    echo 'Bye bye '.$argv[1].'!';

    Y este test:

    <?php
    
    use PHPUnit\Framework\TestCase;
    
    class ScriptTest extends TestCase
    {
            public function testGreeting()
            {
                    $this->assertEquals('Hello World!', shell_exec('php script.php World'));
            }
    }

    Nos dará este resultado:

    PHPUnit 9.3.7 by Sebastian Bergmann and contributors.
    
    F                                                                   1 / 1 (100%)
    
    Time: 00:00.026, Memory: 4.00 MB
    
    There was 1 failure:
    
    1) ScriptTest::testGreeting
    Failed asserting that two strings are equal.
    --- Expected
    +++ Actual
    @@ @@
    -'Hello World!'
    +'Bye bye World!'
    
    /home/mauro/Code/testing/ScriptTest.php:9
    
    FAILURES!
    Tests: 1, Assertions: 1, Failures: 1.

    Conclusión

    En defintiva, no es cierto que es imposible testear aplicaciones php que no se basen en POO… lo que sí es cierto es que los tests van a ser más engorrosos y menos informativos.

    Claro que eso es consecuencia de un diseño poco modular de la aplicación que estamos testeando… ¡pero esa fue precisamente la premisa del ejercicio!

    Espero te haya dado alguna idea nueva, ¡espero tus comentarios!

  • Cómo testear los emails que envía tu aplicación

    Cómo testear los emails que envía tu aplicación

    ¿Alguna vez te llegó un correo similar a este?

    Es un hecho: los mails que genera y envía tu aplicación son una parte de ella y, como tal, deben ser testeados.

    En este artículo te mostraré algunas técnicas que te ayudarán a encarar estas pruebas

    Testear emails enviando a un único destinatario

    Una técnica simple es hacer que todos los correos lleguen siempre a un mismo destinatario (a vos).

    La ventaja de esto es que prácticamente no necesitás nada extra (Nada que no tengas digamos).

    El problema es que implementar esto puede implicar ensuciar el código con condicionales aquí y allí.

    Si usas un componente para el envío (Como el Mailer de Symfony), las cosas pueden ser más simples.

    Basta con especificar el destinatario único en el archivo de configuración de esta forma:

    # config/packages/dev/mailer.yaml
    framework:
        mailer:
            envelope:
                recipients: ['youremail@example.com']

    A partir de aquí, todos los correos salientes serán recibidos por un único destinatario independientemente de quién sea el «verdadero».

    Testear emails usando un servidor SMTP local

    Otra forma de realizar estas pruebas es instalar un servidor SMTP local y configurarlo de modo de no realizar ningún envío, si no encolarlos todos y luego consultar los envíos pendientes.

    Realizar esta configuración desde cero puede resultar bastante engorroso.

    Afortunadamente existen algunas herramientas que simplifican mucho todo este proceso.

    Una que me parece particularmente interesante es MailHog.

    Es bastante simple de instalar y cuenta con una interface web para consultar los correos que se han «enviado» a través de ella:

    Otra ventaja no menor de usar MailHog es que viene pre-instalado con el paquete Homestead de Laravel (Una excelente herramienta para usar una máquina virtual para tus desarrollos)

    Pero, por si no te convencí de usar MailHog aún, acá te dejo algunas alternativas: