1. Code
  2. PHP

Pruebas en paralelo para PHPUnit con ParaTest

Scroll to top

Spanish (Español) translation by Steven (you can also view the original English article)

PHPUnit ha insinuado paralelismo desde 2007, pero, mientras tanto, nuestras pruebas continúan funcionando lentamente. El tiempo es dinero, ¿verdad?. ParaTest es una herramienta que se encuentra sobre PHPUnit y te permite ejecutar pruebas en paralelo sin el uso de extensiones. Este es un candidato ideal para pruebas funcionales (es decir, Selenium) y otros procesos de larga ejecución.


ParaTest a tu servicio

ParaTest es una robusta herramienta de línea de comandos para ejecutar pruebas PHPUnit en paralelo. Inspirado por la buena gente de Sauce Labs, fue desarrollado originalmente para ser una solución más completa para mejorar la velocidad de las pruebas funcionales.

Desde sus inicios, y gracias a algunos colaboradores brillantes (incluido Giorgio Sironi, el responsable de la extensión PHPUnit Selenium), ParaTest se ha convertido en una herramienta valiosa para acelerar las pruebas funcionales, así como las pruebas de integración que involucran bases de datos, servicios web y sistemas de archivos.

ParaTest también tiene el honor de estar incluido en el framework de prueba de Sauce Labs, Sausage, y se ha utilizado en casi 7000 proyectos, al momento de escribir este artículo.

Instalando ParaTest

Actualmente, la única forma oficial de instalar ParaTest es a través de Composer. Para aquellos de ustedes que son nuevos en Composer, tenemos un excelente artículo sobre el tema. Para obtener la última versión de desarrollo, incluye lo siguiente en tu archivo composer.json:

1
"require": {
2
    "brianium/paratest": "dev-master"
3
}

Alternativamente, para la última versión estable:

1
"require": {
2
    "brianium/paratest": "0.4.4"
3
}

Luego, ejecuta composer install desde la línea de comandos. El binario ParaTest se creará en el directorio vendor/bin.

La interfaz de línea de comandos de ParaTest

ParaTest incluye una interfaz de línea de comandos que debería ser familiar para la mayoría de los usuarios de PHPUnit, con algunas bonificaciones adicionales para las pruebas en paralelo.

ParaTest CLIParaTest CLIParaTest CLI

Tu primera prueba en paralelo

Usar ParaTest es tan simple como PHPUnit. Para demostrar rápidamente esto en acción, crea un directorio, paratest-sample, con la siguiente estructura:

sample directory structuresample directory structuresample directory structure

Instalemos ParaTest como se mencionó anteriormente. Suponiendo que tienes un Shell Bash y un binario Composer instalado globalmente, puedes lograr esto en una línea desde el directorio paratest-sample:

1
echo '{"require": { "brianium/paratest": "0.4.4" }}' > composer.json && composer install

Para cada uno de los archivos en el directorio, crea una clase de caso de prueba con el mismo nombre, así:

1
class SlowOneTest extends PHPUnit_Framework_TestCase
2
{
3
    public function test_long_running_condition()
4
    {
5
        sleep(5);
6
        $this->assertTrue(true);
7
    }
8
}

Toma nota del uso de sleep(5) para simular una prueba que tardará cinco segundos en ejecutarse. Así que deberíamos tener cinco casos de prueba, cada uno de los cuales tarda cinco segundos en ejecutarse. Usando vanilla PHPUnit, estas pruebas se ejecutarán en serie y tomarán veinticinco segundos en total. ParaTest ejecutará estas pruebas simultáneamente en cinco procesos separados y solo debería tomar cinco segundos, ¡no veinticinco!

ParaTest vs Vanilla PHPUnitParaTest vs Vanilla PHPUnitParaTest vs Vanilla PHPUnit

Ahora que entendemos qué es ParaTest, profundicemos un poco más en los problemas asociados con la ejecución de pruebas PHPUnit en paralelo.


El problema en cuestión

Las pruebas pueden ser un proceso lento, especialmente cuando empezamos a hablar de acceder a una base de datos o automatizar un navegador. Para realizar pruebas de manera más rápida y eficiente, necesitamos poder ejecutar nuestras pruebas simultáneamente (al mismo tiempo), en lugar de en serie (una tras otra).

El método general para lograr esto no es una idea nueva: ejecutar diferentes grupos de prueba en múltiples procesos PHPUnit. Esto se puede lograr fácilmente usando la función nativa de PHP proc_open. El siguiente sería un ejemplo de esto en acción:

1
/**

2
 * $runningTests - currently open processes

3
 * $loadedTests - an array of test paths

4
 * $maxProcs - the total number of processes we want running

5
 */
6
while(sizeof($runningTests) || sizeof($loadedTests)) {
7
    while(sizeof($loadedTests) && sizeof($runningTests) < $maxProcs)
8
        $runningTests[] = proc_open("phpunit " . array_shift($loadedTests), $descriptorspec, $pipes);
9
    //log results and remove any processes that have finished ....

10
}

Debido a que PHP carece de subprocesos nativos, este es un método típico para lograr cierto nivel de concurrencia. Los desafíos particulares de las herramientas de prueba que utilizan este método se pueden reducir a tres problemas centrales:

  • ¿Cómo cargamos las pruebas?
  • ¿Cómo agregamos e informamos los resultados de los diferentes procesos de PHPUnit?
  • ¿Cómo podemos proporcionar coherencia con la herramienta original (es decir, PHPUnit)?

Veamos algunas técnicas que se han empleado en el pasado y luego repasemos ParaTest y en qué se diferencia del resto de la multitud.


Aquellos que vinieron antes

Como se señaló anteriormente, la idea de ejecutar PHPUnit en múltiples procesos no es nueva. El procedimiento típico empleado es algo similar a lo siguiente:

  • Grep para métodos de prueba o cargar un directorio de archivos que contengan suites de prueba.
  • Abrir un proceso para cada método de prueba o suite.
  • Analizar la salida del pipe STDOUT.

Echemos un vistazo a una herramienta que emplea este método.

Hola, Paraunit

Paraunit fue el corredor paralelo original incluido con la herramienta Sausage de Sauce Labs, y sirvió como punto de partida para ParaTest. Veamos cómo aborda los tres problemas principales mencionados anteriormente.

Prueba de carga

Paraunit fue diseñado para facilitar las pruebas funcionales. Ejecuta cada método de prueba en lugar de un conjunto de pruebas completo en un proceso PHPUnit propio. Dada la ruta a una colección de pruebas, Paraunit busca métodos de prueba individuales, mediante la comparación de patrones con el contenido del archivo.

1
preg_match_all("/function (test[^\(]+)\(/", $fileContents, $matches);

Los métodos de prueba cargados se pueden ejecutar así:

1
proc_open("phpunit --filter=$testName $testFile", $descriptorspec, $pipes);

En una prueba en la que cada método está configurando y derribando un navegador, esto puede hacer que las cosas sean un poco más rápidas, si cada uno de esos métodos se ejecuta en un proceso separado. Sin embargo, hay un par de problemas con este método.

Si bien los métodos que comienzan con la palabra "test" son una fuerte convención entre los usuarios de PHPUnit, las anotaciones son otra opción. El método de carga utilizado por Paraunit omitiría esta prueba perfectamente válida:

1
/**

2
 * @test

3
 */
4
public function twoTodosCheckedShowsCorrectClearButtonText()
5
{
6
    $this->todos->addTodos(array('one', 'two'));
7
    $this->todos->getToggleAll()->click();
8
    $this->assertEquals('Clear 2 completed items', $this->todos->getClearButton()->text());
9
}

Además de no admitir anotaciones de prueba, la herencia también es limitada. Podríamos discutir los méritos de hacer algo como esto, pero consideremos la siguiente configuración:

1
abstract class TodoTest extends PHPUnit_Extensions_Selenium2TestCase
2
{
3
    protected $browser = null;
4
5
    public function setUp()
6
    {
7
       //configure browser

8
    }
9
10
    public function testTypingIntoFieldAndHittingEnterAddsTodo()
11
    {
12
        //selenium magic

13
    }
14
}
15
16
/**

17
 * ChromeTodoTest.php

18
 * No test methods to read!

19
 */
20
class ChromeTodoTest extends TodoTest
21
{
22
    protected $browser = 'chrome';
23
}
24
25
/**

26
 * FirefoxTodoTest.php

27
 * No test methods to read!

28
 */
29
class FirefoxTodoTest extends TodoTest
30
{
31
    protected $browser = 'firefox';
32
}

Los métodos heredados no están en el archivo, por lo que nunca se cargarán.

Visualización de resultados

Paraunit agrega los resultados de cada proceso analizando la salida generada por cada proceso. Este método permite a Paraunit capturar la gama completa de códigos cortos y comentarios presentados por PHPUnit.

La desventaja de agregar resultados de esta manera es que es bastante difícil de manejar y fácil de romper. Hay muchos resultados diferentes que se deben tener en cuenta y muchas expresiones regulares en funcionamiento para mostrar resultados significativos de esta manera.

Coherencia con PHPUnit

Debido al grepping de archivos, Paraunit está bastante limitado en las características de PHPUnit que puede admitir. Es una excelente herramienta para ejecutar una estructura simple de pruebas funcionales, pero, además de algunas de las dificultades mencionadas anteriormente, carece de soporte para algunas características útiles de PHPUnit. Algunos de estos ejemplos incluyen conjuntos de pruebas, especificando archivos de configuración y de arranque, registrando resultados y ejecutando grupos de prueba específicos.

Muchas de las herramientas existentes siguen este patrón. Hacen 'grep' de un directorio de archivos de prueba y ejecutan el archivo completo en un nuevo proceso o cada método, nunca ambos.


ParaTest al bate

El objetivo de ParaTest es admitir pruebas paralelas para una variedad de escenarios. Originalmente creado para llenar los vacíos en Paraunit, se ha convertido en una robusta herramienta de línea de comandos para ejecutar conjuntos de pruebas y métodos de prueba en paralelo. Esto hace que ParaTest sea un candidato ideal para pruebas de larga duración de diferentes formas y tamaños.

Cómo maneja ParaTest las pruebas paralelas

ParaTest se desvía de la norma establecida para admitir más PHPUnit y actúa como un candidato verdaderamente viable para las pruebas paralelas.

Prueba de carga

ParaTest carga las pruebas de manera similar a PHPUnit. Carga todas las pruebas en un directorio específico que terminan con el sufijo *Test.php, o carga las pruebas basadas en el archivo de configuración XML estándar de PHPUnit. La carga se realiza mediante reflexión, por lo que es fácil admitir métodos de @test, herencia, conjuntos de pruebas y métodos de prueba individuales. La reflexión facilita la adición de soporte para otras anotaciones.

Debido a que la reflexión permite que ParaTest tome clases y métodos, puede ejecutar tanto conjuntos de pruebas como métodos de prueba en paralelo, lo que la convierte en una herramienta más versátil.

ParaTest impone algunas limitaciones, pero bien fundamentadas en la comunidad PHP. Las pruebas deben seguir el estándar PSR-0, y el sufijo de archivo predeterminado de *Test.php no es configurable, como está en PHPUnit. Hay una rama actual en progreso para admitir la misma configuración de sufijo permitida en PHPUnit.

Visualización de resultados

ParaTest también se desvía de la ruta de análisis de los pipes de STDOUT. En lugar de analizar los flujos de salida, ParaTest registra los resultados de cada proceso PHPUnit en el formato JUnit y agrega los resultados de estos registros. Es mucho más fácil leer los resultados de las pruebas en un formato establecido que en un flujo de salida.

1
<?xml version="1.0" encoding="UTF-8"?>
2
<testsuites>
3
  <testsuite name="AnotherUnitTestInSubLevelTest" file="/home/brian/Projects/parallel-phpunit/test/fixtures/tests/level1/AnotherUnitTestInSubLevelTest.php" tests="3" assertions="3" failures="0" errors="0" time="0.005295">
4
    <testcase name="testTruth" class="AnotherUnitTestInSubLevelTest" file="/home/brian/Projects/parallel-phpunit/test/fixtures/tests/level1/AnotherUnitTestInSubLevelTest.php" line="7" assertions="1" time="0.001739"/>
5
    <testcase name="testFalsehood" class="AnotherUnitTestInSubLevelTest" file="/home/brian/Projects/parallel-phpunit/test/fixtures/tests/level1/AnotherUnitTestInSubLevelTest.php" line="15" assertions="1" time="0.000477"/>
6
    <testcase name="testArrayLength" class="AnotherUnitTestInSubLevelTest" file="/home/brian/Projects/parallel-phpunit/test/fixtures/tests/level1/AnotherUnitTestInSubLevelTest.php" line="23" assertions="1" time="0.003079"/>
7
  </testsuite>
8
</testsuites>

Analizar los registros de JUnit tiene algunos inconvenientes menores. Las pruebas omitidas e ignoradas no se informan en la retroalimentación inmediata, pero se reflejarán en los valores totales que se muestran después de una ejecución de prueba.

Coherencia con PHPUnit

La reflexión permite que ParaTest admita más convenciones PHPUnit. La consola ParaTest admite más funciones de PHPUnit listas para usar que cualquier otra herramienta similar, como la capacidad de ejecutar grupos, proporcionar archivos de configuración y de arranque, y registrar resultados en el formato JUnit.


Ejemplos de ParaTest

ParaTest se puede utilizar para ganar velocidad en varios escenarios de prueba.

Pruebas funcionales con Selenium

ParaTest sobresale en las pruebas funcionales. Admite un interruptor -f en su consola para habilitar el modo funcional. El modo funcional indica a ParaTest que ejecute cada método de prueba en un proceso separado, en lugar del predeterminado, que es ejecutar cada conjunto de pruebas en un proceso separado.

A menudo ocurre que cada método de prueba funcional hace mucho trabajo, como abrir un navegador, navegar por la página y luego cerrar el navegador.

El proyecto de ejemplo, paratest-selenium, demuestra probar una aplicación Backbone.js todo con Selenium y ParaTest. Cada método de prueba abre un navegador y prueba una función específica:

1
public function setUp()
2
{
3
    $this->setBrowserUrl('https://backbonejs.org/examples/todos/');
4
    $this->todos = new Todos($this->prepareSession());
5
}
6
7
public function testTypingIntoFieldAndHittingEnterAddsTodo()
8
{
9
    $this->todos->addTodo("parallelize phpunit tests\n");
10
    $this->assertEquals(1, sizeof($this->todos->getItems()));
11
}
12
13
public function testClickingTodoCheckboxMarksTodoDone()
14
{
15
    $this->todos->addTodo("make sure you can complete todos");
16
    $items = $this->todos->getItems();
17
    $item = array_shift($items);
18
    $this->todos->getItemCheckbox($item)->click();
19
    $this->assertEquals('done', $item->attribute('class'));
20
}
21
22
//....more tests

Este caso de prueba podría tardar un segundo si se ejecutara en serie, a través de vanilla PHPUnit. ¿Por qué no ejecutar varios métodos a la vez?

running selenium tests with ParaTestrunning selenium tests with ParaTestrunning selenium tests with ParaTest
Many chrome instances running functional testsMany chrome instances running functional testsMany chrome instances running functional tests

Manejo de las condiciones de ejecución

Al igual que con cualquier prueba paralela, debemos tener en cuenta los escenarios que presentarán condiciones de ejecución, como múltiples procesos que intentan acceder a una base de datos. La rama dev-master de ParaTest tiene una función de token de prueba realmente útil, escrita por el colaborador Dimitris Baltas (dbaltas en Github), que hace que las bases de datos de pruebas de integración sean mucho más fáciles.

Dimitris ha incluido un ejemplo útil que demuestra esta característica en Github. En las propias palabras de Dimitris:

TEST_TOKEN intenta abordar el problema de los recursos comunes de una manera muy simple: clona los recursos para garantizar que ningún proceso concurrente acceda al mismo recurso.

Se proporciona una variable de entorno TEST_TOKEN para que las pruebas la consuman y se reciclen cuando el proceso ha finalizado. Se puede usar para alterar condicionalmente tus pruebas, así:

1
public function setUp()
2
{
3
    parent::setUp();
4
    $this->_filename = sprintf('out%s.txt', getenv('TEST_TOKEN'));
5
}

ParaTest y Sauce Labs

Sauce Labs es el Excálibur de las pruebas funcionales. Sauce Labs proporciona un servicio que te permite probar fácilmente tus aplicaciones en una variedad de navegadores y plataformas. Si no los has revisado antes, te recomiendo encarecidamente que lo hagas.

Probar con Sauce podría ser un tutorial en sí mismo, pero esos asistentes ya han hecho un gran trabajo al proporcionar tutoriales para usar PHP y ParaTest para escribir pruebas funcionales usando su servicio.


El futuro de ParaTest

ParaTest es una gran herramienta para llenar algunos de los vacíos de PHPUnit, pero, en última instancia, es solo un enchufe en la presa. ¡Un escenario mucho mejor sería el soporte nativo en PHPUnit!

Mientras tanto, ParaTest continuará aumentando el soporte para más comportamiento nativo de PHPUnit. Continuará ofreciendo características que son útiles para las pruebas en paralelo, particularmente en los ámbitos funcional y de integración.

ParaTest tiene muchas cosas excelentes en proceso para reforzar la transparencia entre PHPUnit y él mismo, principalmente en las opciones de configuración compatibles.

La última versión estable de ParaTest (v0.4.4) es compatible con Mac, Linux y Windows, pero hay algunas solicitudes de extracción valiosas y funciones en dev-master que definitivamente se adaptan a las multitudes de Mac y Linux. Así que será una conversación interesante en el futuro.

Lectura y recursos adicionales

Hay un puñado de artículos y recursos en la web que incluyen ParaTest. Dales una lectura, si estás interesado: