Nuevo en Symfony 2.8: Creando tests sensibles al tiempo

Los denominados "transient tests" son aquellos tests que fallan aleatoriamente dependiendo de factores externos sobre los que no tienes ningún control, como por ejemplo la carga de CPU del sistema en el que se ejecutan. Estos tests son muy peligrosos porque hacen que tu suite de tests no sea fiable (¿los tests fallan por mi culpa o es un error transitorio que se corregirá solo?).

Los tests relacionados con el tiempo son algunos de los tests más comunes de este tipo. Considera por ejemplo el siguiente test:

use Symfony\Component\Stopwatch\Stopwatch;
 
class MyTest extends \PHPUnit_Framework_TestCase
{
    public function testSomething()
    {
        $stopwatch = new Stopwatch();
 
        $stopwatch->start();
        sleep(10);
        $duration = $stopwatch->stop();
 
        $this->assertEquals(10, $duration);
    }
}

Se trata de un test tan sencillo que parece imposible que pueda fallar. Sin embargo, si el ordenador está muy cargado, la variable $duration puede ser 10.00000023 en vez de los 10 exactos que esperamos y este error fallaría sin ningún motivo aparente.

Estos errores son comunes cuando utilizas servicios de integración continua públicos, como Travis CI, donde la carga de los servidores suele ser muy alta. De hecho, en Symfony tenemos un issue para capturar todos estos errores y tratar de solucionarlos.

Manipulando el tiempo

Para solucionar todos estos problema relacionados con el tiempo, el PHPUnit Bridge ahora incluye una clase llamada ClockMock. Esta clase reemplaza las funciones time(), microtime(), sleep() y usleep() de PHP por sus propias funciones.

Como las nuevas funciones se llaman igual y funcionan de la misma manera, cuando utilices ClockMock no hace falta que cambies nada en tu código original. La única excepción es si utilizas new DateTime(), que debes cambiarlo por DateTime::createFromFormat('U', time()) para que use la función time() nueva.

Después, añade una anotación @group especial en aquellos tests en los que quieras manipular el tiempo y usar estas nuevas funciones:

/**
 * @group time-sensitive
 */
class MyTest extends \PHPUnit_Framework_TestCase
{
    public function testSomething()
    {
        $stopwatch = new Stopwatch();
 
        $stopwatch->start();
        sleep(10);
        $duration = $stopwatch->stop();
 
        $this->assertEquals(10, $duration);
    }
}

¡Y ya está! Este test ya nunca volverá a fallar por culpa de las diferencias de tiempo. La llamada a sleep(10) ahora siempre devuelve 10 segundos exactos, por lo que el test siempre será correcto.

Una ventaja añadida de utilizar la clase ClockMock es que el tiempo transcurre instantáneamente. Si usas la función sleep(10) original de PHP, tu test esperará 10 segundos (más o menos). Sin embargo, la función sleep() de la clase ClockMock avanza el tiempo el número de segundos indicado sin tener que esperar. Así que este test ahora se ejecutará 10 segundos más rápido.

Otras formas de activar la manipulación el tiempo

La anotación @group time-sensitive es la forma recomendada de usar la clase ClockMock, pero en tu aplicación podrías tener que usarla de otra manera. La anotación funciona "por convención", ya que supone que el namespace de la clase que se testea se puede obtener simplemente eliminando la parte \Tests\ del namespace de la clase de test.

Aunque esta es la práctica recomendada para aplicaciones Symfony, a lo mejor tu aplicación no la sigue. En ese caso, puedes optar por configurar los namespaces de las clases a manipular mediante el archivo de configuración phpunit.xml. Esto es por ejemplo lo que hacemos en el componente HttpKernel de Symfony:

<listeners>
    <listener class="Symfony\Bridge\PhpUnit\SymfonyTestsListener">
        <arguments>
            <array>
                <element><string>Symfony\Component\HttpFoundation</string></element>
            </array>
        </arguments>
    </listener>
</listeners>

También puedes activar esta funcionalidad de manera explícita. Para ello, llama al método \Symfony\Bridge\PhpUnit\ClockMock::register() dentro del setupBeforeClass() de tu test y pásale la FQCN de la clase a manipular. Después, activa o desactiva esta funcionalidad pasando un argumento booleano al método ClockMock::withClockMock().

Esta funcionalidad ha sido creada por Nicolas Grekas en el Pull Request #16194.

Fuente: New in Symfony 2.8: Clock mocking and time sensitive tests

Comentarios

Publicada el

1 de marzo de 2016

Etiquetas

Proyectos Symfony destacados

La plataforma de eCommerce 100% Symfony que rivaliza con Magento y PrestaShop. Ver más

Síguenos en @symfony_es para acceder a las últimas noticias.