Ya dije en su momento lo importante que consideraba los tests en Javascript, pero que incluirlos en un proceso de integración continua no era una tarea demasiado fácil. Al final es cuestión de ponerse y encontrar las herramientas adecuadas.
En este caso, me voy a centrar en test unitarios y para ello voy a usar PhantomJS, QUnit y MSBuild. Si quisiéramos realizar tests de integración, podríamos recurrir a otras herramientas como Watin o Selenium.
PhantomJS
PhantomJS es un browser sin interfaz gráfica basado en webkit que se ejecuta por línea de comandos y se puede controlar usando un API en Javascript. Gracias a esto, podemos lanzar con él scripts que carguen páginas e interactúen con ellas.
Entre los usos que se le pueden dar a algo tan extraño como esto, están el web scrapping, la monitorización de páginas web o, como en nuestro caso, la ejecución de tests unitarios con frameworks como QUnit.
Para utilizarlo tenemos que descargar la versión de PhantomJS que queramos y a partir de ahí, empezar a jugar.
Integrando PhantomJS con QUnit
Nuestro objetivo es usar PhantomJS para abrir una página con tests para QUnit, dejar que se ejecuten los tests, e inspeccionar el html de la página para extraer el resultado. En función del resultado de los tests estableceremos el valor de salida del proceso a 0 si todos los tests han pasado, o a algo distinto de 0 si ha habido fallos en los tests. Esto hará que desde MSBuild podamos saber si los tests han pasado o no, y actuar en consecuencia.
Hay un ejemplo de script de integración entre PhantomJS y QUnit del que he partido para crear mi script:
if (phantom.args.length === 0 || phantom.args.length > 2) { console.log('Usage: run-qunit.js URL'); phantom.exit(); } function waitFor(operation) { var timeout = operation.timeout || 3000; var start = new Date().getTime(); var completed = false; var interval = setInterval(function() { var ellapsedTime = new Date().getTime() - start; if (operation.isCompleted()) { console.log("script executed in " + ellapsedTime + "ms."); operation.onCompleted(); clearInterval(interval); } else if (ellapsedTime < timeout) { console.log("tests did take too long!"); phantom.exit(1); } }, 100); }; var page = new WebPage(); // Redirect console.log from page to PhantomJS page.onConsoleMessage = function(msg) { console.log(msg); }; page.open(phantom.args[0], function(status){ if (status !== "success") { console.log("Cannot load test page"); phantom.exit(1); } else { waitFor({ isCompleted: function() { return page.evaluate(function(){ var el = document.getElementById('qunit-testresult'); return el && el.innerText.match('completed'); }); }, onCompleted: function() { var failedNum = page.evaluate(function(){ var el = document.getElementById('qunit-testresult'); console.log(el.innerText); $('#qunit-tests > li').each(function() { // This could be written to an xml file to be integrated // with CCNET or other CI servers var module = $('.module-name', this).html(); var name = $('.test-name', this).html(); var failed = $('.failed',this).html(); var passed = $('.passed', this).html(); var total = parseInt(failed) + parseInt(passed); var result = $(this).attr("class"); console.log( (module ? module + "::" : "") + name + " => " + result + "(" + failed + ", " + passed + ", " + total + ")"); }); try { return $('.failed').html(); } catch (e) { } return 10000; }); phantom.exit((parseInt(failedNum) > 0) ? 1 : 0); }, timeout: 10000, }); } });
Para usar este script debemos indicar la ruta al fichero que contiene nuestros tests, por ejemplo:
phantomjs run-qunit.js test/sample.html
El script carga la página que le hayamos indicado dentro de PhantomJS usando el método open
de un objeto WebPage
. El objeto WebPage
expone otro método, evaluate
, que permite ejecutar código Javascript arbitrario dentro de la página.
Con evaluate
podemos revisar cada cierto tiempo si se ha completado la ejecución de los tests, para lo que comprobamos si QUnit ha creado los elementos html que usa para indicar el resultado de los tests. Una vez que se ha completado la ejecución, obtenemos el resultado de los tests, lo mostramos en pantalla y salimos indicando el código de retorno con una llamada a phantom.exit
.
El código quedaría mucho más limpio si no hubiese que utilizar el setInterval
para hacer la comprobación, pero como Javascript se ejecuta en una sola hebra no podemos bloquearla con un bucle de espera y tenemos que recurrir al setInterval
.
Integrando PhantomJS con MSBuild
Hacer que MSBuild ejecute nuestros tests a través de Phantom es muy sencillo usando la tarea Exec:
<Exec Command="phantom run-qunit.js path_to_qunit_test_file.html"/>
Esto hará que se ejecute el script que hemos visto más arriba y se usará el valor de retorno para detener la ejecución del script de MSBuild en caso de que falle algún test.
La versión de run-qunit.js
que he puesto arriba sólo muestra en consola los resultados de los tests, por lo que si quieres que se muestren informes bonitos en tu servidor de integración continua, tendrás que adaptar los resultados al formato que pida.
En mi caso, suelo usar CruiseControl.Net, por lo que es necesario generar la información de salida en formato XML para luego tratarla con una transformada XSLT, pero eso se lo dejo para el que tenga ganas, que tratar con transformadas XSLT es bastante pesado.
Limitaciones
A la hora de usar esta técnica para ejecutar los tests unitarios es importante tener en cuenta que estamos ejecutando los tests sobre webkit, esto quiere decir que podríamos encontrarnos con problemas en navegadores que no estén basados por las diferencias en la implementación de Javascript, como por ejemplo el caso que conté con los problemas del parsing de fechas hace unas semanas.