Desplegando código con Jenkins, Phing, Coder y PHPUnit (Parte III)

04/05/2016
Desplegando código con Jenkins, Phing, Coder y PHPUnit

En un artículo anterior ya vimos como configurar Jenkins para realizar un deployment en un entorno remoto. Además, en una segunda entrega vimos como integrar el Plugin 'Drupal developer' para la validación del coding estandar de Drupal. Ahora, y para finalizar, veremos cómo realizar tests unitarios com PHP Unit integrados dentro de la herramienta Phing.

Para contextualizarnos, diremos que Phing es un port de Apache Ant destinado a la ejecución de tareas, como puede ser la creación de carpetas, actualizaciones sobre un repositoiro, elaborar tests de PHP Unit, etc. todo ello configurado en un archivo XML que después será ejecutado por Jenkins. De esta forma, podemos generar un archivo XML con una serie de tests personalizados que luego se integrarán en el flujo de despliegue de Jenkins. En nuestro ejemplo, vamos a elaborar un test personalizado que ejecutaremos con PHP Unit, para luego poder evaluar los resultados obtenidos del test y poder visualizar el resultado de esta información con el ‘xUnit Plugin’ de Jenkins.

Bien, de antes ponernos a instalar plugins de Jenkins, es necesario instalar Phing en el servidor de integración continua. La mejor manera de instalar Phing es a través de PEAR y del canal PEAR de Phing, así pues hay que entrar en la terminal e introducir los siguientes comandos:

pear channel-discover pear.phing.info
pear install --alldeps phing/phing

pear channel-discover pear.phing.info

Para asegurarnos que la instalación se ha realizado correctamente, podemos ejecutar el siguiente comando en la consola del sistema: 

phing -v

Lo siguiente que tendremos que hacer será crear un fichero en la raíz de nuestro proyecto con el nombre build.xml y ahí escribir las tareas o targets. Si ejecutamos ‘phing’ en la consola del sistema, éste intentará localizar el archivo build.xml en el directorio donde hayamos ejecutado el comando. Si no encuentra este archivo, Phing te devuelve un error indicando que no ha encontrado el archivo.

Lo bueno de las tareas automatizadas es que no son independientes unas de otras, sino que desde una tarea puedes hacer una llamada a otra o incluso pueden existir tareas que dependan de otras para que puedan ser ejecutadas. No es el objetivo de este manual describir con más detalle la estructura del archivo XML ni su relación de dependencias, aunque existe documentación bastante bien detallada sobre su configuración.

El siguiente paso, antes de entrar en el detalle de nuestro archivo XML, será instalar PHPUnit, que es el framework que utilizaremos en este ejemplo para la ejecución de tests unitarios sobre nuestro código. Antes de poder instanciar sus clases y programar la creación de un test, es necesario instalar dicho framework en el sistema. Para ello, podemos descargarlo de su repositorio oficial y ubicarlo dentro de nuestro sistema:

wget https://phar.phpunit.de/phpunit.phar
chmod +x phpunit.phar
sudo mv phpunit.phar /usr/local/bin/phpunit
phpunit --version

 Si todo ha ido bien, el sistema nos reportará la versión de PHP Unit instalada. En nuestro ejemplo, usaremos PHPUnit 4.8.24 junto a la versión 5.4.45 de PHP. Damos por hecho que estamos ejecutando estos tests en una máquina con esta versión de PHP o superior instalada.

Elaborando nuestro primer test

Aunque no es objeto de este manual indagar en el uso de PHP Unit para la elaboración de tests unitarios, diremos que PHPUnit se creó con idea de que cuanto antes se detecten los errores en el código antes podrán ser corregidos. Este conocido framework para PHP nos permite crear y ejecutar juegos de tests unitarios de manera sencilla. Como todos los frameworks de pruebas unitarias, PHPUnit utiliza assertions para verificar que el comportamiento de una unidad de código es el esperado. El objetivo de las pruebas unitarias es aislar cada parte del programa y demostrar que las partes de forma individual son correctas. Una prueba unitaria proporciona un contrato escrito que la pieza de código debe satisfacer. Como resultado, las pruebas unitarias encuentran problemas en las fases iniciales del desarrollo de software.

La idea detras de un assert es crear un objeto, ejecutar algunas funciones y después comprobar su estado interno. Lo mejor es utilizar un ejemplo para ilustrarlo:

class TddTests extends PHPUnit_Framework_TestCase
{
  public function test_tdd_help()
  {
    $this->assertEquals(1,1);
  }
}

Como observamos en el código, básicamente lo que hemos hecho es crear una clase TddTests heredada de la clase PHPUnit_Framework_TestCase. Es aquí dentro donde podremos elaborar la programación de los tests. Existen infinidad de asserts proporcionados por PHPUnit para la elaboración y ejecución de diferentes tipos de tests unitarios, aunque en nuestro ejemplo utilizaremos un caso sencillo mediante el assert assertEquals. Este assert compara dos cadenas de texto pasadas por argumento, y nos devolverá un resultado TRUE/FALSE en función de si estas dos cadenas son iguales (TRUE) o diferentes (FALSE).

Nuestra función test_tdd_help que invoca al assert devolverá en cualquier caso un resultado positivo, ya que los dos argumentos comparativos que le pasamos a nuestro assert son iguales, no obstante, este ejemplo nos permite hacernos una idea del funcionamiento básico de un assert con PHPUnit y el poder que obtenemos con la ejecución de este tipo de tests.

Antes de entrar en la configuración del test unitario a través de Phing, vamos a asegurarnos que nuestro test está bien programado y podemos ejecutarlo sin problemas a través de la consola. De esta forma, si posteriormente nos encontramos con algún problema de configuración, podremos aislar los problemas más fácilmente y detectar con más facilidad el punto de fallo. Para ello, vamos  a crear un archivo llamado TddTests.php ubicado, desde el DOCUMENT ROOT de nuestra instalación de Drupal, en sites/all/modules/main/tdd con el siguiente contenido. Aunque la ubicación o los nombres de los archivos pueden cambiar en función de nuestras necesidades, hemos establecido esta ubicación únicamente para ilustrar el ejemplo:

<?php
define('DRUPAL_ROOT', '/var/www');
require_once DRUPAL_ROOT . '/includes/bootstrap.inc';
drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);

class TddTests extends PHPUnit_Framework_TestCase
{
  public function test_tdd_help()
  {
    $this->assertEquals(1,1);
  }
}
?>

 Las tres primeras líneas cargan el bootstrap de Drupal en caso de que tuviéramos que hacer uso de su API. Aunque en este ejemplo no es estrictamente necesario, lo referenciamos por si el lector quiere ampliar el código de este ejemplo apoyándose en la API de nuestro CMS. El resto contiene la clase que hemos comentado anteriormente, integrando el assert assertEquals que, en todo caso, nos devolverá un resultado positivo.

Ubicándonos de nuevo en el DOCUMENT ROOT de nuestro portal, ejecutamos en consola el siguiente comando:

phpunit sites/all/modules/main/tdd/TddTests.php

Si todo ha ido bien, el framework nos devolverá la siguiente respuesta por pantalla: 

PHPUnit 4.8.24 by Sebastian Bergmann and contributors.

.

Time: 392 ms, Memory: 42.75Mb

OK (1 test, 1 assertion)

En este caso, nos indica que se ha ejecutado un test con un assert, sin ningún error encontrado. Vamos a realizar ahora una modificación en nuestro archivo PHP cambiando el valor de uno de los dos argumentos, de forma que el assert nos devuelva un resultado negativo. Volvemos a ejecutar nuestro comando por consola y ahora el resultado es: 

PHPUnit 4.8.24 by Sebastian Bergmann and contributors.



Time: 392 ms, Memory: 42.75Mb

There was 1 failure:

1) TddTests::test_tdd_help
Failed asserting that 0 matches expected 1.

/var/www/sites/all/modules/main/tdd/TddTests.php:10

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.

 Como vemos en el ejemplo, en esta ocasión, además de informarnos de la ejecución completa del test, nos indica que ha detectado un error, y sabiendo esto, únicamente nos queda recuperar la información resultante del test para poder tratarla con alguna herramienta (¿os imagináis cual?) y poder interactuar con el flujo de despliegue en función de los resultados de los tests.

Volvemos pues al punto de partida con Phing. Ahora que ya sabemos para qué sirve PHP Unit, y hemos comprobado su funcionamiento a través de un sencillo ejemplo a través de un comando por consola, estamos preparados para integrar esta funcionalidad a nuestra herramienta de integración continua, estableciendo su configuración con el apoyo de Phing. Vamos pues a crear nuestro primer archivo build.xml en el raíz de nuestro plataforma, o bien ubicarlo en algún directorio que sea accesible con posterioridad con Jenkins. El contenido del archivo es el siguiente:

<?xml version="1.0"?>
<project name="MyApplication" default="build">
  <property name="package" value="MyApplication" override="true" />
  <target name="clean">
    <delete dir="../reports"/>
  </target>
  <target name="prepare">
    <mkdir dir="../reports/logs"/>
  </target>
  <target name="phpunit">
    <phpunit printsummary="true" haltonfailure="true" pharlocation="/usr/local/bin/phpunit">
      <formatter todir="../reports/logs" type="xml"/>
      <batchtest>
        <fileset dir="../">
          <include name="src/sites/all/modules/main/tdd/TddTests.php"/>
        </fileset>
      </batchtest>
      <formatter type="xml" todir="../reports" outfile="logfile.xml"/>
   </phpunit>
   <phpunitreport infile="../reports/logfile.xml"
                  styledir="/usr/share/php/data/phing/etc"
                  format="frames"
                  todir="../reports"/>
  </target>
  <target name="build" depends="clean,prepare,phpunit"/>
</project>

Como hemos comentado anteriormente, no es objetivo de este manual entrar en profundidad a explicar la creación y configuración de este archivo, no obstante, pasaremos a comentar algunas etiquetas que resultan interesantes para entender su funcionamiento:

  • delete dir y mkdir: le indicamos la ubicación de nuestro directorio de reportes para que lo borre antes y lo vuelva a crear de comenzar la ejecución de nuestros tests
  • phpunit: le indicamos la ruta de nuestro ejecutable PHP Unit
  • formatter todir: indicamos donde deberán almacenarse nuestros reportes, y en qué formato

Posteriormente indicamos la ruta de nuestro test, además de especificar el nombre de nuestro archivo ‘logfile.xml’ con los resultados de nuestros tests.

Como ya hicimos anteriormente con PHPUnit, es hora de probar de forma aislada la composición de nuestro archivo build.xml y comprobar que funciona correctamente antes de integrarlo directamente en Jenkins, de esta forma nos aseguraremos que la estructura de nuestro archivo XML es correcta, y como ya hemos asegurado antes que nuestro test unitario funcionaba correctamente, ahora vamos a subir un nuevo peldaño incluyendo dicha ejecución a través de Phing. Bastaría con ejecutar por consola el siguiente comando en el mismo directorio que hemos creado nuestro archivo build.xml:

phing

 Si nuestro archivo build.xml se encuentra en una ubicación diferente, podemos establecer la ruta del archivo con el parámetro -f. Si todo ha ido correctamente, el resultado por consola debería ser similar al siguiente:

Buildfile: /var/www/build.xml

MyApplication > clean:

   [delete] Deleting directory /var/www/reports

MyApplication > prepare:

    [mkdir] Created dir: /var/www/reports/logs

MyApplication > phpunit:

  [phpunit] Total tests run: 1, Failures: 0, Errors: 0, Incomplete: 0, Skipped: 0, Time elapsed: 0.00471 s

MyApplication > build:


BUILD FINISHED

Total time: 0.1681 seconds

Bien. Llegados a este punto, creo que estamos suficientemente preparados para pasar la ejecución de nuestro test a través de Jenkins, nuestra herramienta de integración continua. Para ello, previamente hay que instalar los módulos ‘Phing Plugin’ para poder ejecutar nuestras tareas a través del archivo build.xml y ‘xUnit Plugin’, que nos permitirá la visualización de las métricas generadas por Phing. No vamos a entrar en más detalles sobre la instalación de Plugins en Jenkins porque ya lo hemos comentado anteriormente. Así que pasamos a crear directamente nuestra tarea, siguiendo los pasos anteriormente con el mismo origen de datos, la misma URL de nuestro repositorio, etc. Nos centraremos en la tarea específica que habrá que crear para ejecutar un script de Phing. Para ello, añadiremos un nuevo paso denominado ‘Invoke Phing Targets’. Este paso debería aparecer automáticamente si hemos instalado correctamente nuestros plugins: 

5.png

El único aspecto aquí a remarcar es el campo ‘Phing Build File’. Simplemente tenemos que indicar la ruta donde hemos creado nuestro archivo build.xml partiendo de nuestro DOCUMENT ROOT. Otro aspecto muy importante a tener en cuenta aquí, y que suele ser la causa de muchos problemas,  es el origen de los datos. Esto quiere decir que para que Jenkins pueda leer correctamente nuestro archivo build.xml, éste debe estar versionado en nuestro repositorio, ya que, recordamos, éste es el origen de datos de nuestra tarea en Jenkins, por lo tanto cualquier cambio que hayamos realizado en nuestro archivo build.xml debe estar correctamente actualizado en nuestro repositorio para que Jenkins pueda ejecutarlo.

El siguiente paso consiste, dentro de la misma tarea, en añadir una nueva acción ‘Publish xUnit test result report’. Al igual que en el paso anterior, esta acción solo estará presente si hemos instalado correctamente nuestros plugins. Pasamos a describir la configuración de esta acción:

6.png

De esta acción vamos a comentar dos aspectos importantes:

  • Ruta de los reportes: es importante recordar que esta ruta debe coincidir con la ruta que hemos configurado en nuestro archivo build.xml de Phing, que es el encargado de generar dichos reportes.
  • Failed Tests: aunque es bastante obvio, me limitaré a comentar que aquí podemos configurar la flexibilidad de nuestros avisos, de forma que podamos configurarlo de tal modo que la tarea pueda resultar exitosa o fallida en función del número de tests que se hayan ejecutado con éxito, etc.

Como comentamos en puntos anteriores, esta tarea puede ser configurada para que sea ejecutada antes o después de otras tareas en nuestro proceso de despliegue, aunque una vez conocida la práctica, creo que en este punto tenemos el conocimiento suficiente como para decidir en qué momento necesitamos ejecutar este test.

Enlaces de interés