Ventajas de PyramidalTests I: Simplicidad, reutilización e historias de usuarios.

PyramidalTests es un framework de PHPUnit que permite que se puedan crear pruebas con el empleo de funciones anónimas anidadas. Este método tiene algunas ventajas frente al método tradicional de PHPUnit que exige que se tenga que crear una clase por cada caso de prueba.

La principal ventaja está en que las pruebas se crean mucho más rápido dado que se que escribe mucho menos código, y además, se facilita la reutilización del mismo dado que por defecto, los casos de pruebas anidados heredarán de sus padres.

Otra de las ventajas que ofrece este método consiste en que las pruebas se podrán describir con un lenguaje natural, lo cual facilitará que con leer el resultado de la ejecución, se pueda comprender mejor el funcionamiento del sistema bajo pruebas(SUT), aunque esto dependerá de lo explícito que logre y/o desee ser el desarrollador al contar en una historia lo que se está probando.

Con el objetivo de demostrar lo antes comentado, vamos a comparar la forma de probar(primero con PyramidalTests y después con PHPUnit) una clase Product que tiene una relación con otra clase Category. Las implementaciones de ambas clases las vamos a obviar pues lo que nos interesa es comparar la forma de uso de ambos frameworks.

El siguiente código muestra las pruebas que se crearon con PyramidalTests:

<?php

use ThenLabs\PyramidalTests\Demo\Product;
use ThenLabs\PyramidalTests\Demo\Category;

testCase('it is created a product', function () {
    setUp(function () {
        $this->product = new Product;
    });

    test('the product has a #CODE#', function () {
        $code = $this->product->getCode();

        $this->assertRegExp('/#\d+#/', $code);
        $this->assertSame($code, $this->product->getCode());
    });

    test('the product not contains categories', function () {
        $this->assertCount(0, $this->product->getCategories());
    });

    testCase('adds a category to the product', function () {
        setUp(function () {
            $this->category = $this->createMock(Category::class);

            $this->product->addCategory($this->category);
        });

        test('the product contains the category', function () {
            $this->assertContains($this->category, $this->product->getCategories());
        });
    });
});

Puede verse que en un único archivo se crearon dos casos de prueba empleando las funciones testCase(), setUp() y test(). Los nombres de estas funciones describen muy bien su respectivo uso ya que están basados en la terminología de PHPUnit. Es de suponer que existen otros como es el caso de setUpBeforeClass(), tearDown() y tearDownAfterClass().

De esta manera, si se ejecuta el comando phpunit con la opción --testdox se obtendrá el siguiente resultado:

Como puede verse, se ejecutaron tres pruebas en dos casos y el resultado contiene los márgenes y los títulos que se especificaron en el código fuente y como fuimos bien descriptivos podemos decir que el resultado nos cuenta en una historia la interacción(o colaboración) que hemos probado de ambas clases.

Si ese mismo resultado lo quisieramos obtener usando PHPUnit puramente no sería posible dado que por defecto los resultados que este muestra no contienen márgenes y además de esto, el orden de ejecución de las clases se determina por el nombre de las mismas y sería muchísimo más trabajoso tener que ir especificando manualmente el orden en el que queremos que se ejecuten nuestras pruebas para que la historia sea contada de la forma deseada.

De todas formas, esos no serían los únicos problemas, y es que para lograr implementar las mismas tres pruebas en dos casos y reutilizando el código, aunque habrían diferentes maneras de hacerlo, ninguna sería tan sencilla como la anterior y esto lo vamos a demostrar seguidamente.

Podríamos pensar que para resolver el tema de la reutilización bastaría con crear dos clases donde una heredaría de la otra pero como vamos a mostrar, eso tampoco nos va a garantizar el resultado deseado.

El siguiente bloque de código muestra la clase del primer caso de prueba.

<?php

namespace ThenLabs\PyramidalTests\Demo\Tests;

use ThenLabs\PyramidalTests\Demo\Product;
use ThenLabs\PyramidalTests\Demo\Category;
use PHPUnit\Framework\TestCase;

/**
 * @testdox it is created a product
 */
class ItIsCreatedAProductTest extends TestCase
{
    public function setUp()
    {
        $this->product = new Product;
    }

    /**
     * @testdox the product has a #CODE#
     */
    public function test1()
    {
        $code = $this->product->getCode();

        $this->assertRegExp('/#\d+#/', $code);
        $this->assertSame($code, $this->product->getCode());
    }

    public function testTheProductNotContainsCategories()
    {
        $this->assertCount(0, $this->product->getCategories());
    }
}

Puede verse que ha sido necesario emplear la anotación @testdox para que el resultado muestre el título en lenguaje natural. En el caso del método test1() es necesaria dicha anotación porque el título contiene el caracter especial # el cual no puede ser empleado en el nombre del método, y en el caso de la anotación sobre la clase, esto se debe a que si no se especifica, PHPUnit mostrará en el resultado el FCQN de dicha clase y no el título deseado tal y como se muestra en la siguiente imagen:

Por otra parte, el siguiente código se corresponde al segundo caso de prueba que como puede verse, hemos especificado que hereda del anterior.

<?php

namespace ThenLabs\PyramidalTests\Demo\Tests;

use ThenLabs\PyramidalTests\Demo\Product;
use ThenLabs\PyramidalTests\Demo\Category;
use PHPUnit\Framework\TestCase;

/**
 * @testdox adds a category to the product
 */
class AddsACategoryToTheProductTest extends ItIsCreatedAProductTest
{
    public function setUp()
    {
        parent::setUp();

        $this->category = $this->createMock(Category::class);

        $this->product->addCategory($this->category);
    }

    public function testTheProductContainsTheCategory()
    {
        $this->assertContains($this->category, $this->product->getCategories());
    }
}

Si una vez llegado a este punto ejecutamos las pruebas vamos a obtener el siguiente resultado inesperado:

Lo primero que vamos a mencionar es que los casos se ejecutaron en orden alfabético y evidentemente esto no es lo que deseamos pues nuestra historia no se estaría contando correctamente. Para resolver este tema podríamos editar el archivo phpunit.xml y especificar el orden de las clases en la respectiva sección testsuite.

<phpunit bootstrap="bootstrap.php">
    ...

    <testsuites>
        <testsuite name="Main">
            <file>tests/ItIsCreatedAProductTest.php</file>
            <file>tests/AddsACategoryToTheProductTest.php</file>
        </testsuite>
    </testsuites>
</phpunit>

De esta manera podríamos obtener el orden deseado tal y como muestra la siguiente imagen:

No obstante, como podemos ver, se ejecutaron en total cinco pruebas cuando en realidad nuestra historia solo se compone de tres, y además de eso, hay una prueba que ha fallado.

Esto se debe a que por defecto PHPUnit va a ejecutar como una prueba a cada método cuyo nombre comience por test, y como hemos usado herencia, la clase del segundo caso habrá heredado los métodos test1() y testTheProductNotContainsCategories() y justamente ese es el motivo por el cual falla este último, y es que como puede verse, en su código hay un acierto que pasará solo si el producto no tiene cateogrías pero en el segundo caso lo que se ha hecho ha sido añadir una categoría al mismo.

Este tema se podría solucionar de varias maneras, por ejemplo, se podrían crear traits para cada método test y desde las clases usar los traits que contengan las pruebas deseadas y de esa manera, se podría garantizar que nuestra historia finalmente se compondrá de las tres pruebas deseadas.

Una vez que hemos llegado a este punto, estaremos de acuerdo en que habremos cumplido con el objetivo de demostrar que usar PyramidalTests puede ahorrar muchísimo trabajo frente al método tradicional de PHPUnit y que además, nos permite escribir pruebas mucho más claras que podrán contar historias.

A este tipo de desarrollo se le suele llamar desarrollo guiado por comportamiento(BDD) y como PyramidalTests está basado en PHPUnit, podemos decir que lo que hace es extender las posibilidades del mismo al campo BDD.

En la próxima publicación vamos a mostrar otras ventajas muy útiles que ofrece PyramidalTests cuando se están creando pruebas funcionales y/o de integración.

Fecha de publicación: 2021-05-14

Si desea enterarse por correo electrónico sobre nuestras próximas publicaciones y novedades en general, suscríbase a nuestro boletín.

Redes Sociales

Suscríbase también a nuestras redes sociales.
También por esos canales compartiremos nuestras novedades.

Contactar

Para contactarnos puede escribirnos a:
thenlabs@gmail.com