Pruebas unitarias en Joomla con PHPUnit y soporte para debugging

Es necesario un dominio básico de pruebas unitarias, si las has realizado utilizando otro framework como JUnit esto servirá.
Bueno, mi primera impresión con PHPUnit ha sido positiva, sin haber profundizado aún he podido observar características que no había visto en Junit aunque derepente existan, como dependencia entre métodos de pruebas unitarias para evitar repetir el código que normalmente escribimos para establecer el entorno en el que se realizará una prueba (mock objects por ejemplo) y los proveedores de datos (data providers).

Recomiendo la siguiente guía bastante completa y didáctica. Aunque para el fin de seguirme en el presente tutorial no será necesario, ya lo podrán hacer después para profundizar si desean.

http://www.phpunit.de/manual/current/en/automating-tests.html

Comenzaremos por la instalación de PHPUnit usando los repositorios PEAR.

Ejecutaremos los siguientes comandos si tenemos a pear dentro de la variable de entorno PATH.

pear channel-discover pear.phpunit.de
pear channel-discover pear.symfony-project.com
pear install phpunit/PHPUnit

Es necesario mencionar que se requiere cierta versión de PEAR y algunas dependencias por lo cual podría ser necesario realizar la instalación de la siguiente manera:

pear upgrade-all
pear channel-discover pear.phpunit.de
pear channel-discover pear.symfony-project.com
pear install --alldeps phpunit/PHPUnit

Si se encuentran detrás de un proxy:

pear config-set http_proxy http://ip:puerto

Ahora necesitamos agregar el repositorio local de librerías PEAR al include_path si no lo habíamos hecho antes. Aquí pueden ver las instrucciones:

http://support.modwest.com/content/5/98/en/how-do-i-set-php-include_path.html

Ahora podemos pasar a la configuración del IDE y joomla.
Utilizaremos el eclipse PDT que es el que he encontrado con mejor soporte para funcionalidades avanzadas como debugging (También pueden usar aptana con el PHp debugger como extensión).

Después de instalarlo en alguna ubicación y abrirlo en un workspace nos dirigiremos a window > preferences > PHP > PHP Libraries y creamos una librería a la que llamaremos PEAR y luego haciendo clic en Add External folder agregaremos la carpeta raíz del repositorio PEAR, esto nos otorgará la capacidad de autocompletado de las referencias hacia la librería pear y además el debugging de pruebas unitarias.

Bueno, con el IDE y PEAR correctamente configurado podemos comenzar a hacer pruebas unitarias en joomla. Para el soporte de debugging de pruebas unitarias asumiré que tienen soporte para usar Zend Debugger aunque supongo que también debería funcionar con XDebug.  (digo: supongo) Un servidor web con php con soporte para Zend Debugger por defecto es el http://www.zend.com/en/products/server-ce/

Pasos a seguir:

1. Crear instalación de joomla 1.5.x nueva.

Esta importante que joomla sea de la serie 1.5.x porque no he probado con ninguna otra, aunque seguramente no requeriría mucha esfuerzo modificar los scripts para que funcione en las versiones 1.x por citar un ejemplo.

2. Importar esta nueva instalación al PDT como un nuevo proyecto, en mi caso joomla ha sido instalado en la carpeta newsite.

New PHP Project > Create project from existing source

3. Creamos un nuevo proyecto para las pruebas unitarias correspondientes a la instalación de joomla mencionada arriba, lo llamaré newsitetests, recomiendo ubicarlo en la misma carpeta en la que se encuentra newsite (nótese que cree una carpeta vacía con el nombre newsitetests para poder crear el proyecto a partir de fuentes existentes).

4. Ahora dentro del proyecto de las pruebas unitarias crearemos una estructura como la siguiente, nótese lo que he hecho:

- Agregué la instalación de joomla (newsite) y la librería PEAR al PHP Include Path.

Ahora, una pequeña explicación del significado de cada uno de los archivos:

JRequest-0000-getmethod-Test.php Nuestra prueba unitaria de ejemplo.
JoomlaTestCase.php Este archivo contiene una clase que extiende a PHPUnit_Framework_TestCase para agregar soporte para joomla, Joomla_PHPUnit_Framework_TestCase
JoomlaTestCase.xml xml de configuración que Joomla_PHPUnit_Framework_TestCase espera a través de su constructor
NewsiteBaseTestContext.php Contiene una clase que extiende a Joomla_PHPUnit_Framework_TestCase con la configuración específica para nuestro proyecto de pruebas.
xml2array.php Librería externa que se puede encontrar en http://blog.unijimpe.net/xml2array-php-xml-parser/
phpunit.php script de lanzamiento de phpunit que se obtiene con la instalación PEAR que se realizó al comienzo.

Pueden bajarse el proyecto de pruebas newsitetests desde aquí:

http://www.megaupload.com/?d=3LDIQUSN

Aquí solo publicaré el código fuente de JoomlaTestCase.php, el cual contiene la clase que deben extender las clases de pruebas unitarias para obtener el soporte especial para joomla.

<?php

require_once 'PHPUnit/Framework.php';
require_once 'xml2array.php';

/**
 * It extends PHPUnit_Framework_TestCase to preload all required
 * joomla stuff
 *
 * @author JHABLUTZEL
 *
 */
class Joomla_PHPUnit_Framework_TestCase extends PHPUnit_Framework_TestCase
{

 // properties, need to set in constructor
 protected $configurationPath = null;

 // properties cominf from $configurationPath
 protected $joomlaDir = null;
 protected $joomlaSite = null;
 protected $joomlaAdminUser = null;
 protected $joomlaAdminPassword = null;

 /**
 * Path to configuration xml
 */
 public function setConfigurationXML($configurationXML){
 $this->configurationPath = $configurationXML;
 }

 private function loadConfiguration() {
 $contents = file_get_contents($this->configurationPath);
 $result = xml2array($contents);
 $this->joomlaDir =         $result['configuration']['joomlaDir']['value'];

 }

 public function __construct($name = NULL, array $data = array(), $dataName = '', $configurationXML)
 {

 parent::__construct($name, $data, $dataName);
 $this->configurationPath = $configurationXML;

 if (!isset($configurationXML) || $configurationXML == null) {
 throw new Exception("JoomlaTestCase needs a configurationXML");
 }

 $this->loadConfiguration();

 ini_set('include_path',ini_get('include_path').';' . $this->joomlaDir ); // only works in windows, change to : as include_pat separator for unix

 define( '_JEXEC', 1 );
 define('JPATH_BASE', $this->joomlaDir );
 define( 'DS', DIRECTORY_SEPARATOR );
 require_once ( JPATH_BASE .DS.'includes'.DS.'defines.php' );
 require_once ( JPATH_BASE .DS.'includes'.DS.'framework.php' );
 }

}

Creé esta clase basandome en la manera en la que AbstractJUnit4SpringContextTests de Spring, lo hace en el mundo Java, esta clase debe ser extendida por una clase propia del proyecto, en nuestro caso, la clase que se encuentra en NewsiteBaseTestContext.php.

5. Ahora debemos agregar soporte para correr las pruebas unitarias desde el eclipse PDT. Precisamente para esto copie phpunit.php, el cual no es nada más que el script de inicio agregado por PEAR cuando se instaló PHPUnit, éste método podría ser cuestionado, la otra manera de hacerlo es usando External Tools de PDT, pero de ésta manera no he conseguido soporte para debugging. Ese método lo podrían encontrar aquí:

http://www.phpunit.de/wiki/Eclipse

Usando mi método creamos una configuración de debugging del tipo PHP Script.

Y creo que ésto es todo ya pueden seleccionar una prueba unitaria como la que se subió con el ejemplo JRequest-0000-getmethod-Test.php e iniciar el debugging.

Y al final, el resultado:

Ésta última sección podría resultar algo confusa para alguien que no sea familiar con Eclipse PDT, pero les recomiendo enérgicamente familiarizarse con esta herramienta, que es muy buena.

Vínculos de referencia:

http://docs.joomla.org/Unit_Testing

Nota:

He subido el archivo de ejemplo en un repositorio libre como es megaupload, sin embargo podría suceder que ellos lo borren por estar utilizando el servicio gratuito, si alguno de ustedes conoce un mejor servicio de repositorio de archivos le agradeceré que me lo diga ;)

Pruebas unitarias usando mock objects

Hoy comenzaré el día escribiendo un post después de casi dos meses lejos del blog, ha sido un tiempo largo pero ha sido aprovechado para aprender nuevas tecnologías e historias que compartir por aquí.

El tema de hoy será como escribir pruebas unitarias usando mock objects, pero, ¿qué son los mock objects? Yo los definiría como objetos programables para que se comporten de una manera determinada en runtime. ¿Y para qué puede servirnos esto al hacer pruebas unitarias? Nos servirá para probar las clases aisladas de sus dependencias, así, si una de las clases de las que nuestra clase depende falla, entonces este error no afectará la prueba unitaria de nuestra clase y deberemos identificar y solucionar ese problema en la otra clase (que debería tener escritas algunas pruebas unitarias).

Veamos un ejemplo en el que las depedencias pueden fallar.

Clases con dependencias La clase que queremos probar es ManejoDeUsuariosImpl, una implementación de la interface ManejoDeUsuarios, ésta clase se compone de instancias de LDAPAccesibleImpl, esto significa que si algo falla en LDAPAccesibleImpl nuestra clase también fallará probablemente y al hacer pruebas unitarias lo que queremos es someter a prueba exclusivamente el comportamiento de una clase independientemente del correcto funcionamiento de sus dependencias. Para darnos una mejor idea… ¿Qué podría fallar en LDAPAccesibleImpl? Hmm creo que todo, podría fallar la conexión de red al servidor LDAP, podría suceder que hayan eliminado al usuario que pensabamos usar para nuestras pruebas unitarias, etc.

Bueno, veamos un ejemplo de prueba unitaria para nuestra clase ManejoDeUsuariosImpl que es la implementación de ManejoDeUsuarios.

ManejoDeUsuarioImplTest.java

package com.imageneureka;

import org.junit.Test;

import static org.easymock.EasyMock.*;

public class ManejoDeUsuariosImplTest {

 private static final String USUARIO_ACTIVO = "active";
 private static final String USUARIO_NO_EXISTENTE = "inactive";

 @Test
 public void hacerAlgoConUnUsuarioActivoTestHappyPath()
 throws Exception {
 // primer paso: creamos el mock, usando el metodo
 //estatico org.easymock.EasyMock.createMock
 LDAPAccesible foo = createMock(LDAPAccesible.class);
 // segundo paso: establecemos el comportamiento
 //, parametros -> respuesta
 expect(foo.esUnUsuarioActivo(USUARIO_ACTIVO)).
 andReturn(Boolean.TRUE).anyTimes();
 // invocamos replay sobre el mock
 replay(foo);
 ManejoDeUsuariosImpl bar = new ManejoDeUsuariosImpl();
 // necesitamos esta linea para establecer la instancia
 //de LDAPAccesible que usara
 // nuestra clase a probar, en este caso el mock
 bar.setMyLDAPAccesibleImpl(foo);
 bar.hacerAlgoConUnUsuarioActivo(USUARIO_ACTIVO);
 // invocamos verify sobre el mock
 verify(foo);
 }

 @Test(expected=IllegalArgumentException.class)
 public void hacerAlgoConUnUsuarioActivoTestLLamadaIncorrecta()
 throws Exception {
 // primer paso: creamos el mock, usando el metodo estatico
 //.easymock.EasyMock.createMock
 LDAPAccesible foo = createMock(LDAPAccesible.class);
 // segundo paso: establecemos el comportamiento,
 //parametros -> respuesta
 expect(foo.esUnUsuarioActivo(USUARIO_NO_EXISTENTE)).
 andReturn(Boolean.FALSE).anyTimes();
 // invocamos replay sobre el mock
 replay(foo);
 ManejoDeUsuariosImpl bar = new ManejoDeUsuariosImpl();
 // necesitamos esta linea para establecer la instancia
 //de LDAPAccesible que usara
 // nuestra clase a probar, en este caso el mock
 bar.setMyLDAPAccesibleImpl(foo);
 bar.hacerAlgoConUnUsuarioActivo(USUARIO_NO_EXISTENTE);
 // invocamos verify sobre el mock
 verify(foo);
 }
}

Antes de que revisemos el ejemplo les sugiero bajarse el ejemplo completo para que puedan ver la dependencia entre las clases de implementacion y asi puedan darse una idea mas clara de qué es lo que pretende testear esta prueba unitaria.

http://www.4shared.com/file/XfzNNqwc/easyMockTest.html

En el archivo que bajarán encontrarán una carpeta con un archivo pom.xml, éste es el project object model de maven, para los que no conocen maven, es un manejador de dependencias y proyectos muy útil, con él solamente necesitan el pom.xml y las dependencias (jars) se bajan de internet automáticamente, además ya se está convirtiendo en el estándar para gestionar proyectos, por lo tanto: http://www.chuidiang.com/java/herramientas/maven.php además tiene muy buena integración con los IDEs que conozco, a saber Netbeans y Eclipse, en Netbeans tan solo tiene que ir a instalar plugins seleccionar el plugin para maven y ya esta, listo para usar.

Regresando a los nuestro, en esta prueba unitaria hay dos métodos de prueba, uno de ellos (hacerAlgoConUnUsuarioActivoTestHappyPath() ) prueba la clase bajo circuntancias favorables (usuario válido) y el otro (hacerAlgoConUnUsuarioActivoTestLLamadaIncorrecta()) prueba la llamada al método ManejoDeUsuariosImpl.hacerAlgoConUnUsuarioActivo() con un usuario incorrecto, circunstancia bajo la cual éste metodo debería responder con una excepción IllegalArgumentException y el proceso de emulación del comportamiento de la dependencia de la clase a testear se detalla como comentarios en el ejemplo.

Por último, cabe mencionar que easyMock no es el único framework para crear mock objects, también existe JMock, el cual implementa ésta funcionalidad aunque con un estilo DSL