Pruebas paralelas para PHPUnit con ParaTest

PHPUnit ha sugerido el paralelismo desde 2007, pero, mientras tanto, nuestras pruebas continúan ejecutándose lentamente. El tiempo es dinero, ¿verdad? ParaTest es una herramienta que se encuentra en la parte superior de PHPUnit y le permite ejecutar pruebas en paralelo sin el uso de extensiones. Este es un candidato ideal para pruebas funcionales (es decir, selenio) y otros procesos de larga duración..


ParaTest a su servicio

ParaTest es una herramienta de línea de comandos robusta para ejecutar pruebas de 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 su inicio, y gracias a algunos colaboradores brillantes (incluido Giorgio Sironi, el mantenedor 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 incluyen bases de datos, servicios web y sistemas de archivos.

ParaTest también tiene el honor de estar incluido en el marco de pruebas Sausage de Sauce Labs, y se ha utilizado en casi 7000 proyectos, en el 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 gran artículo sobre el tema. Para obtener la última versión de desarrollo, incluya lo siguiente dentro de su compositor.json expediente:

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

Alternativamente, para la última versión estable:

 "require": "brianium / paratest": "0.4.4"

A continuación, ejecute instalación del compositor desde la línea de comando. El binario de ParaTest se creará en el vendedor / bin directorio.

La interfaz de línea de comandos de ParaTest

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

Tu primera prueba paralela

Usar ParaTest es tan simple como PHPUnit. Para demostrar rápidamente esto en acción, cree un directorio, muestra paratest, Con la siguiente estructura:

Instalemos ParaTest como se mencionó anteriormente. Suponiendo que tiene un shell Bash y un binario Composer instalado globalmente, puede lograr esto en una línea desde la muestra paratest directorio:

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

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

 la clase SlowOneTest extiende PHPUnit_Framework_TestCase public function test_long_running_condition () sleep (5); $ this-> assertTrue (true); 

Tomar nota del uso de dormir (5) Para simular una prueba que tardará cinco segundos en ejecutarse. Por lo tanto, deberíamos tener cinco casos de prueba que demoren cinco segundos en ejecutarse. Usando la unidad de PHP de vainilla, 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 tomará cinco segundos, no veinticinco!

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


El problema a mano

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

El método general para lograr esto no es una idea nueva: ejecutar diferentes grupos de prueba en múltiples procesos de 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:

 / ** * $ runningTests - procesos abiertos actualmente * $ loadedTests - una matriz de rutas de prueba * $ maxProcs - el número total de procesos que queremos que se ejecuten * / while (sizeof ($ runningTests) || sizeof ($ loadedTests)) while (sizeof ($ loadedTests) && sizeof ($ runningTests) < $maxProcs) $runningTests[] = proc_open("phpunit " . array_shift($loadedTests), $descriptorspec, $pipes); //log results and remove any processes that have finished… 

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 usan este método pueden reducirse a tres problemas principales:

  • ¿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 revisemos ParaTest y sus diferencias con respecto al resto de la multitud..


Los que vinieron antes

Como se señaló anteriormente, la idea de ejecutar PHPUnit en varios procesos no es nueva. El procedimiento típico empleado es algo en las siguientes líneas:

  • Grep para métodos de prueba o cargar un directorio de archivos que contengan suites de prueba.
  • Abra un proceso para cada método de prueba o suite.
  • Analiza la salida de la tubería STDOUT.

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

Hola paraunit

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

Carga de prueba

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

 preg_match_all ("/ function (prueba [^ \ (] +) \ (/", $ fileContents, $ coincidencias);

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

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

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

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

 / ** * @test * / public function twoTodosCheckedShowsCorrectClearButtonText () $ this-> todos-> addTodos (array ('one', 'two')); $ this-> todos-> getToggleAll () -> click (); $ this-> assertEquals ('Borrar 2 elementos completados', $ this-> todos-> getClearButton () -> text ()); 

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

 la clase abstracta TodoTest extiende PHPUnit_Extensions_Selenium2TestCase protected $ browser = null; función pública setUp () // configure browser public function testTypingIntoFieldAndHittingEnterAddsTodo () // selenium magic / ** * ChromeTodoTest.php * ¡No hay métodos de prueba para leer! * / class ChromeTodoTest extiende TodoTest protected $ browser = 'chrome';  / ** * FirefoxTodoTest.php * ¡No hay métodos de prueba para leer! * / class FirefoxTodoTest extiende TodoTest protected $ browser = 'firefox'; 

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 al analizar la salida generada por cada proceso. Este método le 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 a tener en cuenta y muchas expresiones regulares en el trabajo para mostrar resultados significativos de esta manera.

Consistencia con PHPUnit

Debido al archivo grepping, Paraunit es bastante limitado en cuanto a 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 funciones útiles de PHPUnit. Algunos de estos ejemplos incluyen suites de prueba, especificación de archivos de configuración y arranque, resultados de registro y ejecución de grupos de prueba específicos.

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


ParaTest At Bat

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

Cómo ParaTest maneja las pruebas paralelas

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

Carga de prueba

ParaTest carga las pruebas de manera similar a PHPUnit. Carga todas las pruebas en un directorio específico que termina con el * Test.php sufijo, o cargará pruebas basadas en el archivo de configuración estándar de PHPUnit XML. La carga se realiza a través de la reflexión, por lo que es fácil de soportar @prueba Métodos, herencia, suites de prueba y métodos de prueba individuales. La reflexión hace que agregar soporte para otras anotaciones sea un complemento.

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

ParaTest impone algunas restricciones, pero bien fundamentadas en la comunidad de PHP. Las pruebas deben seguir el estándar PSR-0 y el sufijo de archivo predeterminado de * Test.php no es configurable, como lo es en PHPUnit. Hay una rama actual en curso 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 tuberías STDOUT. En lugar de analizar las secuencias 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 de un formato establecido que un flujo de salida.

        

El análisis de 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 prueba.

Consistencia con PHPUnit

Reflection permite a ParaTest admitir más convenciones de PHPUnit. La consola de ParaTest admite más funciones de la unidad PHP fuera de la caja que cualquier otra herramienta similar, como la capacidad de ejecutar grupos, suministrar archivos de configuración y bootstrap 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 selenio

ParaTest sobresale en las pruebas funcionales. Es compatible con un -F cambiar en su consola para habilitar el modo funcional. El modo funcional le indica a ParaTest que ejecute cada método de prueba en un proceso separado, en lugar del predeterminado, que consiste en ejecutar cada serie de pruebas en un proceso separado.

Es frecuente que cada método de prueba funcional haga mucho trabajo, como abrir un navegador, navegar por la página y luego cerrar el navegador..

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

 función pública setUp () $ this-> setBrowserUrl ('http://backbonejs.org/examples/todos/'); $ this-> todos = new Todos ($ this-> prepareSession ());  función pública testTypingIntoFieldAndHittingEnterAddsTodo () $ this-> todos-> addTodo ("paralelizar pruebas phpunit \ n"); $ this-> assertEquals (1, sizeof ($ this-> todos-> getItems ()));  función pública testClickingTodoCheckboxMarksTodoDone () $ this-> todos-> addTodo ("asegúrate de que puedes completar todos"); $ items = $ this-> todos-> getItems (); $ item = array_shift ($ items); $ this-> todos-> getItemCheckbox ($ item) -> click (); $ this-> assertEquals ('hecho', $ item-> attribute ('clase'));  //… más pruebas

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

Condiciones de carrera de manejo

Al igual que con cualquier prueba paralela, debemos tener en cuenta los escenarios que presentarán condiciones de carrera, como los procesos múltiples que intentan acceder a una base de datos. La rama dev-master de ParaTest tiene una característica muy útil de token de prueba, 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 tratar el problema de los recursos comunes de una manera muy simple: clona los recursos para asegurarte de que ningún proceso concurrente tendrá acceso al mismo recurso.

UNA TEST_TOKEN La variable de entorno se proporciona para que las pruebas se consuman y se recicla cuando el proceso ha finalizado. Se puede utilizar para alterar condicionalmente sus pruebas, de esta manera:

 función pública setUp () parent :: setUp (); $ this -> _ filename = sprintf ('out% s.txt', getenv ('TEST_TOKEN')); 

ParaTest y Sauce Labs

Sauce Labs es el Excalibur de las pruebas funcionales. Sauce Labs proporciona un servicio que le permite probar fácilmente sus aplicaciones en una variedad de navegadores y plataformas. Si no los ha revisado antes, le recomiendo que lo haga.

Las pruebas con Sauce podrían 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 tapón en la represa. Un escenario mucho mejor sería el soporte nativo en PHPUnit!

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

ParaTest tiene muchas cosas buenas en el trabajo para reforzar la transparencia entre PHPUnit y él mismo, principalmente en qué opciones de configuración son compatibles.

La última versión estable de ParaTest (v0.4.4) es compatible con Mac, Linux y Windows, pero hay algunas solicitudes y funciones de extracción valiosas en maestro de desarrollo que sin duda atender a las multitudes de Mac y Linux. Así que esa será una conversación interesante en el futuro..

Lectura adicional y recursos

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

  • ParaTest en Github
  • Paralelo PHPUnit por el colaborador de ParaTest y mantenedor de la extensión PHPUnit Selenium Giorgio Sironi
  • Contribuyendo a Paratest. Un excelente artículo sobre el WrapperRunner experimental de Giorgio para ParaTest
  • Código fuente de Giorgio's WrapperRunner
  • Tripsta / paratest-muestra. Un ejemplo de la característica TEST_TOKEN por su creador Dimitris Baltas
  • brianium / paratest-selenio. Un ejemplo del uso de ParaTest para escribir pruebas funcionales.