Mostrando entradas con la etiqueta unit test. Mostrar todas las entradas
Mostrando entradas con la etiqueta unit test. Mostrar todas las entradas

miércoles, 27 de febrero de 2013

Algunas notas para tener en cuenta en Test Unitarios

Evitar errores definiendo Fixtures con algunos tipos de campos

Por alguna razón que ignoro, al definir los campos (var $fields) en fixtures con los tipos text, date o datetime (sospecho que también binary) es mejor no usar el array completo para definir todos los parámetros, sino únicamente poner el tipo de campo. De otro modo, CakePHP se empeña en crear una definición de tabla SQL que no es válida y el test falla. Al dejar que lo haga de manera automática va perfecto.

Por ejemplo:

var $fields = array(
    'content' => 'text',
    'created' => 'date'
);

Tests y localización

Añade la instrucción

Configure::write('Config.language', 'eng');

al principio del test para evitar que CakePHP localice las cadenas de texto con __() y similares. De este modo, si un resultado del test depende de una cadena localizada no la traducirá (asumiendo que uses eng como idioma por defecto, claro).


martes, 13 de marzo de 2012

Crear Models y Fixtures para Tests

Una de mis frustraciones con CakePHP es que la documentación para crear Test Unitarios no cubre bien todas las posibilidades. Encontrar algunas soluciones concretas requiere bastante investigación en el manual, API, código del core y en blogs dispersos.

Uno de estos puntos oscuros (para mí) es el Test de Behaviors. Los Behaviors, como sabrás, son extensiones a los Models. Normalmente para crear los tests necesitarás un modelo al que asociarlos y el problema es que, por lo general, este modelo debería ser independiente y específicamente creado para la ocasión, o lo bastante genérico como para no introducir factores indeseados en el proceso de test.

¿Cómo crear esos modelos? Pues esto es lo que voy a intentar contar en este artículo. Esto lo he aprendido estudiando el código de tests de Cake y, aunque no tengo claro que sea todo lo completo que desearía, al menos he conseguido que vaya funcionando para mis propósitos.

Models

Para empezar, los Model para Tests extienden la clase CakeTestModel (que a su vez desciende de Model y se ajusta para utilizar la configuración de base de datos de test por defecto),
Pero nuestros Models no deberían acceder a una base de datos "física", a fin de no introducir elementos de distorsión de los resultados del test.

Esto se consigue definiendo el esquema o estructura de datos en el propio modelo y especificando que no se use una tabla. Aquí tienes un ejemplo:

class Basic extends CakeTestModel {
    var $useTable = false;
    var $name = 'Basic';
    var $_schema = array(
        'id'=> array('type' => 'string', 'null' => '', 'default' => '1', 'length' => '36', 'key'=>'primary'),
        'title'=> array('type' => 'string', 'null' => '', 'default' => '', 'length' => '255'),
        'key'=> array('type' => 'string', 'null' => '1', 'default' => '', 'length' => '255'),
    );
}


El lenguaje de esquema de CakePHP es bastante sencillo y te permite definir los tipos de campos que necesites. En este caso, necesitaba dos campos de texto, aparte del campo id.

Pero, ¿dónde defino este modelo?

Pues se me ocurren dos ubicaciones. Si no piensas reutilizarlo, podrías definirlo en el mismo archivo que el test.

Pero si crees que puedes utilizarlo en tests de otros elementos de tu aplicación (otros Behaviors, Controllers, etc) es buena idea guardarlo en un archivo separado e incluirlo cuando sea necesario.
En mi caso, he creado un archivo models.php en la carpeta tests de la aplicación. En este archivo colecciono los modelos creados para tests. Para incluirlo, no tengo más que usar este código:

require_once(TESTS . DS . 'models.php');

Y luego crear una instancia del modelo cuando la necesite con los métodos habituales, ya sea mediante new Model o ClassRegistry.


Los datos

En la mayoría de los tests necesitaré datos en los modelos, para lo cual tendré que crear un archivo de fixtures.

En teoría debería bastar con utilizar la propiedad CakeTestFixture->import y especificar que vamos a usar el modelo creado, pero yo no he conseguido hacerlo funcionar así, por lo que he tenido que especificar los campos del modelo en el archivo de fixtures (basta con copiar y pegar el contenido de Model->_schema). Puede que sea debido al hecho de no utilizar una tabla en la base de datos, sin embargo, no deja de ser un engorro. La parte buena es que estos modelos no tienen que cambiar mucho y realmente no da tanto trabajo.

Incluimos también los registros (records) que necesitemos para empezar. Este es un ejemplo de TEST/fixtures/basic_fixture.php


class BasicFixture extends CakeTestFixture {
    var $name = 'Basic';
    var $fields = array(
        'id'=> array('type' => 'string', 'null' => '', 'default' => '1', 'length' => '36', 'key'=>'primary'),
        'title'=> array('type' => 'string', 'null' => '', 'default' => '', 'length' => '255'),
        'key'=> array('type' => 'string', 'null' => '1', 'default' => '', 'length' => '255'),
    );
   var $records = array(
        array(
            'id' => 1,
            'title' => 'Lorem ipsum dolor sit amet',
            'key' => 'lorem_ipsum_dolor_sit_amet',
        ),
    );
}
?>


En el test declaramos que vamos a usar como fixtures app.basic y así estamos listos para trabajar. Aquí tienes un ejemplo de un test que utiliza el modelo Basic.

Conclusiones

Finalmente, tener modelos para usarlos específicamente en tests unitarios es bastante fácil. Son autocontenidos, por lo que no necesitas tener una base de datos y los puedes reutilizar dentro de un mismo proyecto o en otros.

martes, 20 de julio de 2010

Hacer tests de un método que crea un objeto que debemos simular (una alternativa a los partial Mock)

(Editado para corregir un error grave)

Supongamos que tenemos un método en una clase y que dentro de ese método se crea un objeto, el cual debemos simular para hacer el test. Algo así como esto:


public function getFeed($url) {
    $Socket = ClassRegistry::init('HttpSocket');
    $Socket->reset(false);
    $response = $Socket->get($url);
    ...
}


En la documentación de SimpleTest se analiza ese caso y se proponen varias soluciones. Pero en el caso de CakePHP he descubierto una que es sencilla y resuelve el problema de una forma muy eficaz y elegante.

HttpSocket es una clase muy a propósito para ser simulada, ya que requiere conectarse a un servidor y no podemos garantizar que sea posible hacerlo en la situación de test, por lo que lo lógico sería usar la simulación. Pero tal como está escrito el método, el objeto se crea y se utiliza (e incluso se destruye) en el ámbito del propio método. En la interfaz de éste no hay forma de pasar el objeto, como se puede ver, por lo que habría que reescribir el código para poder escribir el test.

ClassRegistry al rescate

Para empezar, tendremos que instanciar el objeto con ClassRegistry en lugar de con el tradicional new object() de PHP. ClassRegistry es una factoría de clases que ofrece algunos servicios interesantes. Además de crear los objetos, mantiene un registro, de modo que si una clase es instanciada varias veces, y no indicamos lo contrario, no crea un objeto nuevo cada vez, sino que devuelve el existente, ahorrando memoria.

Esto se hace con el método init, de esta forma:


$Post = ClassRegistry::init('Post');


Con este método se crea una entrada en el registro que pone la clase Post bajo la clave 'Post', devolviendo un objeto de clase Post por referencia.

Pero ClassRegistry tiene otro método que nos interesa: addObject. Este método nos permite poner en el registro cualquier objeto bajo la clave que deseemos.


ClassRegistry::addObject('Post', $MockedPost);


Para el caso que nos ocupa podemos crear el Mock que necesitamos, instanciarlo y pasarlo a ClassRegistry con la clave de la clase que estamos simulando. La próxima vez que se llame a ClassRegistry::init, devolverá el Mock.

El siguiente bloque de código muestra cómo hacerlo:


Mock::generate('HttpSocket');
$Socket = ClassRegistry::init('MockHttpSocket');
ClassRegistry::addObject('HttpSocket', $Socket);


La primera línea genera la simulación de HttpSocket con el nombre MockHttpSocket.

La segunda línea instancia la clase simulada, no es imprescindible usar ClassRegistry, pero tampoco es mala práctica.

Por último, la tercera línea, hace la magia, añadiendo el objeto que acabamos de instanciar en la clave HttpSocket del registro.

Lo anterior es la preparación. A continuación habría que establecer los valores de respuesta que necesitemos, etc.

Luego vendría la llamada para probar el método:


$result = $this->Feed->getFeed($url);


Este es, de nuevo, el fragmento del método que quería probar:


public function getFeed($url) {
    $Socket = ClassRegistry::init('HttpSocket');
    $Socket->reset(false);
    $response = $Socket->get($url);
    ...
}

Al "inyectar" en ClassRegistry la clase simulada MockHttpSocket bajo la clave HttpSocket consigo que en situación de test se utilice el Mock.

Y todo ello sin tocar el código de la clase.

jueves, 10 de junio de 2010

Simulando objetos con Mock Objects

Entre otras facilidades, la Test Suite de CakePHP utiliza la capacidad de SimpleTest de crear Mock Objects, o lo que es lo mismo, simulaciones de objetos que no hacen nada, pero que reproducen todos los métodos de los objetos simulados y que puedes programar para que tengan un comportamiento determinado que te interese.

Esto es útil cuando quieres probar una clase que usa otras clases. En lugar de emplear las clases reales, utilizamos sus simulaciones. De este modo, nos evitamos las posibles interferencias de su funcionamiento sobre los resultados del test y garantizamos que sólo probamos el código de la clase en cuestión.

Un ejemplo sencillo típico sería tratar de probar un Controller que usa un Component.

Lo que tenemos que hacer son básicamente dos cosas:

  • Crear una simulación o Mock del Component
  • Asociar el Component simulado al Controller

Para crear una simulación de una clase usamos Mock::generate('Clase'). Esto nos crea una clase que podemos instanciar para asociar a la clase testada.

Imagina que quieres probar PostsController, habiendo declarado Auth como Component. El código necesario sería algo así como:


App::import('Controller', 'Posts');
App::import('Component', 'Auth');

class PostsTestCase extends CakeTestCase {
   
    var $Posts;
   
    function startTest() {
        $this->Posts = ClassRegistry::init('PostsController');
        Mock::generate('AuthComponent');
        $this->Posts->Auth = new MockAuthComponent();
    }
}

?>


El código de startTest hace lo siguiente:

En primer lugar, instancia PostsController y lo pone en la variable Posts de PostsTestCase, así lo tenemos disponible en cualquiera de los métodos del test.

Seguidamente, se genera el Mock de AuthComponent.

Finalmente, asociamos el AuthComponent simulado a $this->Posts, de manera que haga el papel que haría el AuthComponent real.

Si fuese necesario, tendríamos que simular o hacer a mano cualquier otra operación que fuese necesaria para reproducir el funcionamiento normal de las clases implicadas.

Los métodos de AuthComponent están simulados en MockAuthComponent, pero no hacen nada. Para que el Mock sea útil necesitaremos indicarle que devuelva ciertos datos al llamar a alguno de sus métodos. Para esto utilizamos el método setReturnValue(), de este modo:


...
$this->Posts->Auth->setReturnValue('user', array('User' => array(...)));
...


La línea anterior le indica al objeto simulado que cuando se llame a su método 'user' devuelva el valor indicado. Gracias a esto podemos simular determinados escenarios que nos interesa probar.

Los parámetros que pueda tener el método son indiferentes en este caso. Sin embargo, es posible indicar a setReturnValue que tenga en cuenta los argumentos del método (esto lo dejaré para otro artículo, pero lo puedes encontrar en la documentación de SimpleTest).

Por ejemplo, ¿qué pasa si no hay un usuario autentificado en la acción edit? Para hacerlo, necesitamos indicarle al Auth simulado, que el método 'user' devuelva false:


...
$this->Posts->Auth->setReturnValue('user', false);
$result = $this->Posts->edit(123);
...


Con el código anterior conseguimos simular que Auth->user no devuelve ningún valor (no hay usuario autentificado) y así probamos cómo responde nuestro método a esa situación. Seguidamente podemos hacer otro test, simulando que sí existe un usuario autentificado.

Si fuese necesario podemos "anidar" objetos simulados. Es decir, si tenemos que simular una clase que contiene otras clases, podemos simular estas y asociarlas.

Para simular propiedades (variables de clase) no tenemos más que asignarles el valor deseado.

Ahora, supón que un método de un objeto simulado es llamado varias veces en el método probado y necesitas que cada vez devuelva un valor distinto. En este caso, debes pasarle primero los valores con setReturnValueAt.

$objeto->setReturnValue('value', false);
$objeto->setReturnValueAt(0, 'value', 'Sample');
$objeto->setReturnValueAt(1, 'value', 'Another sample');


En resumen, utilizando Mock Objects pueden prescindir de las dependencias de la clase probada, lo que hará que tus tests sean más flexibles, fiables y precisos.

domingo, 13 de septiembre de 2009

Unit-testing y yo

Hace unos pocos posts comenté que una de las estrategias que estaba adopotando en mi desarrollo, junto a la modularización por plugins, es el unit-test y el desarrollo dirigido por tests.

Como programador autodidacta uno de los grandes problemas que encuentro es aprender de una forma sistemática y fundamentada. Uno no dispone de la estructuración que una formación más reglada te puede proporcionar. De este modo, temas como los patrones de programación o el uso de los test en el proceso de desarrollo no te resultan evidentes al principio, a veces, ni siquiera comprensibles.

Bueno, que me enrollo. Yo quería hablar de cómo llegué al punto en el que efectivamente uso los tests para probar mi software y cada vez más sigo una metodología dirigida por ellos. Pero quizá convenga empezar por explicar que es eso de los tests.

Test: probando el software

Al principio uno escribe trozos de código y ejecuta el programa para ver qué pasa. Es una manera básica de probar un software. De hecho, eso es básicamente un test de software: ejecutarlo y ver si ofrece los resultados esperados.

Cuando empecé con CakePHP, gracias a que proporciona una base completa para una aplicación, hacía más o menos eso mismo.

Estos tests "a ojímetro" no funcionan tan mal en situaciones sencillas, pero en cuanto las cosas empiezan a complicarse se vuelven ineficientes en progresión geométrica. En un momento dado resultan inútiles.

Cuando aparece un error en la aplicación, puede que haya fallado dentro de una función o método, pero es muy posible que la causa del error esté muy lejos en la pila de llamadas. Es decir, que el error real esté en un lugar y con la información disponible no tengamos forma de encontrarlo sin revisar toda la aplicación.

Eso sin mencionar la dificultad, por ejemplo, de probar los múltiples escenarios en que una aplicación puede funcionar. ¿De qué manera puedes saber que has probado el efecto de introducir ciertos datos de diferentes maneras? ¿Alguno de ellos puede generar un error?

Y, finalmente, si realizas un cambio de código, ¿cómo vuelves a probar todo otra vez para asegurarte de que el cambio no tiene efectos indeseados en otra parte?

Se hace necesario utilizar una metodología más sistemática y eficaz, que permita replicar las pruebas las veces que haga falta en multiplicidad de condiciones. Aquí es donde entra el Unit-testing.

Unit-rest: prueba de unidades de software

Como dice el título del apartado, el unit-test es una metodología de prueba de software que se basa en la prueba aislada de las unidades mínimas en que podemos dividir nuestro código. En el caso de CakePHP, que es un framework orientado a objetos, esas unidades son los métodos de las diferentes clases que componen la aplicación.

Por otro lado, los test serían programas que se encargan de llamar a las distintas unidades con diferentes condiciones (parámetros que se pasan, constantes globales, etc.) y comparar el resultado que ofrecen con los resultados que esperamos. Por ejemplo, si una función calcula el doble de un número, el test consistiría en algo así como:

$resultado = dobleDe(100);
    $esperado = 200;
if($resultado == $esperado) {
    echo 'OK';
} else {
    echo 'algo falla en dobleDe';
}

Al ser un programa podemos repetirlo cuantas veces queramos, en especial si hacemos algún cambio en la función dobleDe(), lo que nos diría si nuesto cambio o refactor está afectando al funcionamiento del código.

También podríamos probar multitud de valores para asegurarnos de que la función devuelve los valores correctos, sobre todo en ciertos puntos críticos. Por ejemplo, una función para calcular el precio con descuento por volumen en una tienda podría tener los intervalos:

Unidades                  descuento
menos de 10 ud.       0 %
entre 10 y 20 ud       3% de descuento
más de 20 ud            5% de descuento

Aquí tendríamos que probar al menos los siguientes valores de unidades:

<10 =10 >20 y <20 =20 >20

Es decir, tendríamos que escribir al menos 5 test variando las unidades de producto para ver si la función nos devuelve el precio correcto para una combinación de producto y cantidades.

Ayudas al Unit-Test

Por supuesto, escribir los tests "a pelo" es un trabajo considerable. Para ayudar en la tarea existen bibliotecas como SimpleTest, en la cual se basa el Test Suite de CakePHP.

En conjunto la Test Suite nos proporciona un entorno para probar las clases que escribimos para nuestra aplicación, garantizándonos el mínimo de funcionalidad que necesitamos para que nuestros modelos, controladores y vistas puedan ser probados, así como funciones específicas para hacer los tests.

Un concepto básico son las aserciones o asserts. Se trata de afirmaciones que hacemos sobre el resultado de una unidad. Por ejemplo, que el resultado va a ser igual a cierto valor, que será cierto o falso, o que se coincidirá con una determinada expresión regular.

CakePHP tiene una clase CakeTestCase que incorpora la mayoría de asserts que podemos necesitar, así, podremos escribir un test como el siguiente:

$result = $this->Post->find('count');
$this->assertEqual($result, 5):

Que básicamente quiere decir que el find('count') debería encontrar 5 registros en la base de datos que estamos usando de prueba.

Otra ayuda importante son los Mock Objects. Estos objetos nos permiten imitar el comportamiento de objetos de nuestra aplicación, pero sin que ejecuten su código real, sino que ofrecen la misma intefaz y podemos programarlos para que devuelvan ciertos resultados que nos interesen.

Es decir, que si el método que estamos probando llama a un objeto "mockeado", no se ejecutará el código del objeto original, sino que el "mock" nos devolverá el valor que le hemos configurado que devuelva.

Eso nos permite aislar el código que estamos probando del resto de la aplicación, lo que hace más fiable el test (ejecuta sólo el código que probamos) y nos permite jugar con diferentes escenarios.

Un ejemplo típico es hacer un Mock del EmailComponent. Puede que en nuestra máquina de test no podamos enviar correo usando el EmailComponent, pero haciendo un Mock podemos simular que lo ha enviado y basarnos en eso para probar una parte de la aplicación. O también podemos probar la condición de que no funciona y ver cómo la supera nuestra aplicación.

También es posible probar condiciones de error. Es posible utilizar la función expectError para detectar que nuestro código produce un error. Por ejemplo, cuando lanzas un error desde el código si los datos que llegan a un método son inválidos.

La asserts, además, permiten a la Test Suite realizar algunas estadísticas con tus test. De este modo, puedes tener un número de tests sobre una clase y saber cuántos pasan, cuántos fallan y si se han provocado excepciones.

Desarrollo dirigido por tests

El desarrollo dirigido por tests es una metodología en que usas el Unit-testing como base para desarrollar tus aplicaciones. Se trata de escribir los tests antes que el código de las unidades.

¿Cómo?

Sí, al principio me costó mucho entender esta idea, que ahora me parece de lo más evidente.

En el fondo, escribir un test para una unidad de software es definir de una manera formal sus especificaciones e interfaz: qué parámetros debe recibir y qué resultados ha de proporcionar y en qué formato.

Esto puede hacerse antes de escribir el código, por supuesto. Tú sabes lo que quieres que haga un método antes de escribirlo. En realidad, en un equipo de desarrollo, ni siquiera tendría que ser la misma persona la que prepara los test y la que realiza el código.

Preparar el test te hace pensar muy a fondo en la interfaz del método. Y escribir código para cumplir el test te obliga a estar muy enfocado en lo que estás haciendo. Y, sobre todo, te proporciona una red de seguridad para el futuro.

Tests y refactor

En un momento dado te plantearás refactorizar el código. Los tests te ayudan a garantizar que no se rompe nada. Es genial, en serio: incorporas unas modificaciones y pruebas, si falla, revisas de nuevo y reescribes, vuelves a probar, y así sucesivamente hasta que vuelves a pasar el test. Y, si no, puedes volver a la revisión anterior que sí funcionaba.

Si se trata de añadir funcionalidades nuevas, los test también te ayudan. Por un lado, los tests originales te garantizan que el nuevo código no rompe la funcionalidad original. Por otro lado, debes añadir tests que prueben las nuevas características.

Unit-test y calidad de vida

Pues mejora mucho. Me costó llegar a realizar tests para las diferentes clases. Tiene su complicación probar un controlador por ejemplo, o un behavior. Sin embargo, superadas esas dificultades (lo que a su vez me permitió aprender mucho acerca de cómo funciona CakePHP), el resultado no puede ser mejor.

Mi código está mejor escrito y más pensado. Mi trabajo es más focalizado en objetivos concretos. Además, debido a que con frecuencia tengo que interrumpir el desarrollo para dedicarme a otras actividades, me resulta mucho más fácil retomar el trabajo después de un tiempo. También me permite, por ejemplo, dedicar ratos sueltos a resolver pequeños problemas y avanzar en los proyectos, tomando algún problema detectado o algún test fallido y viendo cómo resolverlo.

Al poder trabajar con partes aisladas, no tienes miedo a romper la aplicación y tener muchos frentes abiertos. Te centras en una tarea y la resuelves, luego otra y luego otra.

La inversión de tiempo en aprender a usar los test y en crearlos realmente merece la pena.

Saber si haces buenos tests

El Test Suite incluye soporte para analizar la cobertura de código de tus test mediante Xdebug. Me costó un poco preparar mi sistema para poder utilizarlo y ha sido una especie de revelación cuando lo conseguí. Además, Xdebug mejora la información que te devuelve la aplicación cuando hay errores de PHP, mostrándote la pila de llamadas y diversa información.

La cobertura de código te indica qué porcentaje del código probado es ejecutado en realidad. Te ayuda a descubrir partes del código que no se ejecutan, condiciones que no has probado y otros muchos detalles, pues te muestra los fragmentos no ejecutados.

Al disponer de esta información es más fácil hacer tests para todo tipo de condiciones, o saber si tienes que crear nuevos tests aunque cubras el 100% del código con los que tienes, al añadir nuevas funcionalidades o hacer refactor de un método.

Los límites de los test

Los test no son la solución para garantizar un programa perfectamente libre de errores. Los test te informan de que una unidad de software hace lo que esperas que haga, suponiendo que has cubierto todos los escenarios posibles con los tests.

Sin embargo, la tranquilidad y seguridad que te proporcionan, la focalización que te aportan y la objetividad y claridad que te dan a la hora de definir tus tareas de programación, tienen un valor que compensan claramente estos límites.