Acelera la ejecución de tus tests con PHPUnit

8 de septiembre de 2014

Para que una suite de tests unitarios y funcionales sea útil, debe ser muy rápida al ejecutarse. Si desarrollas tus aplicaciones siguiendo metodologías como el TDD o desarrollo guiado por pruebas, entonces la rapidez no es recomendable sino totalmente obligatoria.

Algunos autores sugieren que todos los tests de la aplicación deberían ejecutarse en 10 segundos o menos. Por eso en este artículo te explicamos varias técnicas para acelerar la ejecución de tus tests con PHPUnit.

Recomendaciones básicas

1. No crees más fixtures de los que necesitas. ¿Para qué insertas 10.000 registros con datos de prueba en la base de datos si a tus tests les basta con tener 20 registros? Genera siempre el mínimo número posible de datos de prueba.

2. No regeneres la base de datos de prueba para cada test. Aunque en tus tests utilices bases de datos SQLite en memoria, regenerarlas para cada test es un proceso muy costoso en suites con miles de tests.

La solución consiste en generar una única base de datos SQLite al comienzo de todos los test y llenarla con todos los datos de prueba. Después, antes de cada test haces una copia de ese archivo para tener una base de datos completamente nueva. Esta técnica es tan habitual entre programadores, que si utilizas Symfony existe un bundle llamado LiipFunctionalTestBundle que lo hace por ti automáticamente.

3. No abuses de los métodos setUp() y tearDown(). Estos métodos te permiten ejecutar código antes y después de cada test, por lo que son ideales para inicializar objetos y eliminar cualquier elemento generado por los tests.

El problema es que estos métodos se ejecutan, respectivamente, antes y después de cada test de cada clase, por lo que pueden ralentizar mucho su ejecución. Por eso PHPUnit define también los métodos setUpBeforeClass() y tearDownAfterClass(), que solamente se ejecutan, respectivamente, antes del primer test de la clase y después del último test. Si has diseñado bien tus clases de test, seguramente podrás reemplazar setUp() y tearDown() por estos otros métodos.

4. No abuses del dataProvider. Esta característica de PHPUnit te permite ejecutar repetidamente un mismo test utilizando diferentes datos de entrada cada vez. Como resulta tan sencillo añadir nuevos datos de prueba, algunos programadores abusan del dataProvider añadiendo decenas de datos de entrada para aumentar así el número total de asserts y parecer que has hecho muchos tests.

En realidad tus tests sólo deberían probar los (pocos) casos esperados por tu código y los (pocos) casos límites que podrían romper tu código. Imagina que quieres testear un método que incrementa el stock de un producto en unidades enteras positivas. Los casos a probar podrían ser solamente los siguientes:

/**
 * @dataProvider provideIncrementosDeStock
 */
public testIncrementaStock($unidades)
{
    // ...
}

public function provideIncrementosDeStock()
{
    return [
        // estos son los casos esperados (números enteros positivos)
        [1], [5],
        // estos son los casos límite (ceros, negativos, decimales, letras)
        [-3], [2.3], [0], [null], ['a']
    ];
}

Plugins para aumentar el rendimiento de PHPUnit

Al margen de las recomendaciones relacionadas con el propio código de tus tests, se han publicado varios plugins para PHPUnit orientados a mejorar su rendimiento. La mayoría de ellos ya vendrán incluidos cuando se publiquen las próximas versiones de PHPUnit.

Plugin PHPUnit Accelerator

La idea de este plugin consiste en acelerar la ejecución de los tests reduciendo su consumo de memoria. Gracias a esta técnica sus autores aseguran que se pueden acelerar los tests hasta un 20%.

Para instalarlo, ejecuta el siguiente comando dentro del directorio de tu proyecto (este comando requiere que Composer esté instalado globalmente en tu sistema):

$ composer require "mybuilder/phpunit-accelerator":"~1.0"

Para activarlo, solamente debes añadir lo siguiente en el archivo de configuración de PHPUnit (que normalmente se llama phpunit.xml y se encuentra en la raíz del proyecto):

<phpunit>
    <!-- ... -->

    <listeners>
        <!-- ... -->

        <listener class="\MyBuilder\PhpunitAccelerator\TestListener"/>
    </listeners>
</phpunit>

Este plugin ha sido creado por Keyvan Akbary, de la empresa MyBuilder, y ha sido publicado en: github.com/mybuilder/phpunit-accelerator

Plugin SpeedTrap

Como seguramente sabes, lo que no se puede medir no se puede mejorar. Así que la idea de este plugin es mostrarte cuáles de tus tests son inaceptablemente lentos. Así podrás atacar directamente a los peores tests y conseguir rápidamente reducciones drásticas en el tiempo de ejecución de los tests.

Para instalarlo, ejecuta el siguiente comando dentro del directorio de tu proyecto:

$ composer require "johnkary/phpunit-speedtrap":"~1.0@dev"

Para activarlo, añade lo siguiente en el archivo de configuración de PHPUnit:

<phpunit>
    <!-- ... -->

    <listeners>
        <!-- ... -->

        <listener class="JohnKary\PHPUnit\Listener\SpeedTrapListener" />
    </listeners>
</phpunit>

La próxima vez que ejecutes los tests, verás una lista con todos aquellos que consumen más de 500 milisegundos al ejecutarse:

$ phpunit

PHPUnit 4.2.4 by Sebastian Bergmann.

Configuration read from librosweb.es/phpunit.xml

.....................................................  20 / 100 ( 20%)
.....................................................  40 / 100 ( 40%)
.....................................................  60 / 100 ( 60%)
.....................................................  80 / 100 ( 80%)
..................................................... 100 / 100 (100%)

You should really fix these slow tests (>500ms)...
 1. 2517ms to run Librosweb\Tests\...Test:testLoremIpsum()
 2. 778ms to run Librosweb\Tests\...Test:testLoremIpsum()
 3. 621ms to run Librosweb\Tests\...Test:testLoremIpsum()
 4. 1598ms to run Librosweb\Tests\...Test:testLoremIpsum()

Si tus tests son buenos, seguramente 500 milisegundos es un valor demasiado alto, así que puedes configurarlo con la opción slowThreshold. Por su parte, la opción reportLength indica el número de tests que deben mostrarse en la lista de tests lentos:

<phpunit>
    <!-- ... -->

    <listeners>
        <!-- ... -->

        <listener class="JohnKary\PHPUnit\Listener\SpeedTrapListener">
            <arguments>
                    <array>
                        <element key="slowThreshold">
                            <integer>20</integer>
                        </element>
                        <element key="reportLength">
                            <integer>30</integer>
                        </element>
                    </array>
                </arguments>
        </listener>
    </listeners>
</phpunit>

Si en tu aplicación tienes algunos tests especiales que requieren mucho más tiempo del habitual, puedes añadir la anotación @slowThreshold para indicar el tiempo máximo permitido para ese test específico:

class MiTest extends \PHPUnit_Framework_TestCase
{
    /**
     * @slowThreshold 5000
     */
    public function testEsteTestEsMuyLento()
    {
        // este test puede tardar hasta 5 segundos sin levantar sospechas
        // ...
    }
}

Este plugin ha sido creado por John Kary y ha sido publicado en: github.com/johnkary/phpunit-speedtrap.

Plugin Paratest

El objetivo de este plugin es aprovechar las CPUs modernas que soportan varios hilos de ejecución simultáneos para ejecutar todos tus tests en paralelo.

Para instalarlo, ejecuta el siguiente comando dentro del directorio de tu proyecto:

$ composer require "brianium/paratest":"dev-master"

A diferencia de los anteriores este plugin no se activa mediante el archivo de configuración de PHPUnit, sino que se ejecuta a través de su propio comando llamado paratest. Composer instala este comando en el directorio vendor/bin/, por lo que sólo debes ejecutar lo siguiente:

$ ./vendor/bin/paratest

Por defecto este comando ejecuta tus tests en cinco procesos paralelos. En las pruebas que he realizado, el tiempo de ejecución total no se reduce cinco veces, pero sí que baja prácticamente a la mitad.

Si tu máquina lo soporta, puedes aumentar el número de procesos en paralelo mediante la opción --processes:

$ ./vendor/bin/paratest --processes=10

Este plugin ha sido creado por Brian Scaturro y ha sido publicado en: github.com/brianium/paratest.

Referencias útiles