jueves, 21 de agosto de 2014
Symfony - Día 8: Pruebas Unitarias
Durante las últimas dos semanas hemos revisado todas las funciones aprendidas durante los cinco primeros días del calendario de Jobeet para personalizar y añadir nuevas funciones. En el proceso, también hemos tocado otras funciones más avanzadas de symfony.
Hoy, vamos a empezar hablando de algo completamente diferente:pruebas automáticas. Como el tema es bastante grande, nos llevará dos días completos para cubrir todo.
Las Pruebas en Symfony
Existen dos tipos de pruebas automáticas en symfony: Pruebas Unitarias y Pruebas Funcionales.
Las Pruebas Unitarias verificar que cada método y función está trabajando correctamente. Cada pruebas deberá ser lo más independiente posible de las demás.
Por otro lado, Pruebas Funcionales verifican que la aplicación resultante se comporta correctamente en todo su conjunto.
Todas las pruebas en symfony están ubicadas bajo el directorio
test/
del proyecto. Este tiene a su vez dos sub-directorios, uno para pruebas unitarias (test/unit/
) y otro para las pruebas funtionales (test/functional/
).
Las Pruebas Unitarias se cubrirán en el tutorial de hoy, mientras que en el de mañana se dedicará a las Pruebas Funcionales .
Pruebas Unitarias
Escribir pruebas unitarias es quizás una de las mejores prácticas de desarrollo web, más difíciles de poner en práctica. Como los desarrolladores web realmente no las utilizan para poner a prueba su trabajo, muchas preguntas surgen: ¿Tengo que escribir las pruebas antes de la implementación de una función? ¿Qué necesito para hacer la prueba? ¿Mis pruebas necesitan cubrir todos y cada uno de los casos de uso? ¿Cómo puedo estar seguro de que todo está bien probado? Pero frecuentemente, la primer pregunta es la más básica: ¿Donde empezar?
Incluso si eres un fervoroso partidario de las pruebas, el enfoque de symfony es pragmático: siempre es mejor disponer de algunas pruebas que no tener ninguna. ¿Ya tienes un montón de código sin ningún tipo de prueba? No hay problema. No es necesario disponer de un completo conjunto de pruebas para beneficiarse de las ventajas de ellas. Empieza por agregar pruebas cada vez que encuentras un fallo en el código. Con el tiempo, el código será mejor, el código aumentará, y serás un desarrollador con mayor confianza en tí mismo. Empezando con un enfoque más pragmático, te sentirás más cómodo con las pruebas con el paso del tiempo. El siguiente paso es escribir las pruebas de las nuevas características. En breve tiempo, te convertirás en un adicto a las pruebas.
El problema con la mayoría de las bibliotecas de pruebas es su empinada curva de aprendizaje. Es por eso que symfony proporciona una muy simple librería para pruebas, lime, para hacer la escritura de pruebas increíblemente fácil.
Aún si este tutorial describe extensamente la librería lime que viene incorporada en Symfony, puedes utilizar cualquier librería para pruebas, como la excelente librería PHPUnit.
El Framework de Pruebas lime
Todas las pruebas unitarias escritas con el framework lime comienzan con el mismo código:
require_once dirname(__FILE__).'/../bootstrap/unit.php'; $t = new lime_test(1);
Primero, el archivo incluído
unit.php
hace la inicialización de un par de cosas. A continuación, un nuevo objeto lime_test
se crea y el número de pruebas que planeamos lanzar se pasa como argumento.
El plan permite a lime mostrar un mensaje de error aún en caso de que sean pocas las pruebas que se ejecutan (por ejemplo, cuando una prueba genera un error fatal de PHP). Las Pruebas implican llamar a un método o a una función con un conjunto predefinido de argumentos y, a continuación, comparar la salida con los resultados esperados. Esta comparación determina si una prueba pasa (aprueba) o no.
Para facilitar la comparación, el objeto
lime_test
proporciona varios métodos:Método | Descripción |
---|---|
ok($test) | Prueba una condición y pasa si es true |
is($value1, $value2) | Compara dos valores y pasa si son iguales (== ) |
isnt($value1, $value2) | Compara dos valores y pasa si son distintos |
like($string, $regexp) | Prueba una cadena contra una expresión regular |
unlike($string, $regexp) | Comprueba que la cadena difiera de la expresión regular |
is_deeply($array1, $array2) | Comprueba que dos arrays tienen los mismos valores |
Puedes preguntarte por qué lime define tantos métodos de prueba, si todas las pruebas se pueden escribir solo usando el método
ok()
. El beneficio de los métodos alternativos estan en los mensajes de error mucho más explícitos en caso de que una prueba falle y en la mejora de la legibilidad de las pruebas.
El objeto
lime_test
también ofrece otros convenientes métodos de prueba:Método | Descripción |
---|---|
fail() | Siempre falla -útil para probar las excepciones |
pass() | Siempre pasa -útil para probar las excepciones |
skip($msg, $nb_tests) | Cuenta como $nb_tests pruebas -para pruebas condicionales |
todo() | Cuenta como una prueba -útil para pruebas aun no escritas |
Por último, el método
comment($msg)
muestra un comentario o mensaje pero no realiza ninguna prueba.Ejecutando Pruebas Unitarias
Todas las pruebas unitarias son guardadas en el directorio
test/unit/
. Por convención, las pruebas son nombradas con el nombre de la clase que ellas prueban más el sufijo Test
. Puedes organizar los archivos en el directorio test/unit/
de la forma que deseas, te recomendamos replicar la estructura de directorios del directorio lib/
.
Crea un archivo
test/unit/JobeetTest.php
y copia en él, el siguiente código:// test/unit/JobeetTest.php require_once dirname(__FILE__).'/../bootstrap/unit.php'; $t = new lime_test(1); $t->pass('This test always passes.');
Para lanzar las pruebas, puedes ejecutar el archivo directamente:
$ php test/unit/JobeetTest.php
O usar la tarea
test:unit
:$ php symfony test:unit Jobeet
Los comandos de linea de Windows desafortunadamente no pueden resaltar los resultados de la prueba en colores rojo ni verde. Pero su utilizas Cygwin, puedes forzar a Symfony a usar colores pasando la opción
--color
a la tarea.
Probando slugify
Vamos a comenzar nuestro viaje al maravilloso mundo de las pruebas unitarias escribiendo las pruebas para el método
Jobeet::slugify()
.
Creamos el método
slugify()
durante el día 5 para limpiar una cadena para que pueda ser seguro incluírla en una URL. La conversión consiste en algunas básicas transformaciones como la de convertir todos los carácteres no-ASCII en un guión (-
) o convertir la cadena a minúsculas:Entrada | Salida |
---|---|
Sensio Labs | sensio-labs |
Paris, France | paris-france |
Reemplaza el contenido del archivo de pruebas con el siguiente código:
// test/unit/JobeetTest.php require_once dirname(__FILE__).'/../bootstrap/unit.php'; $t = new lime_test(6); $t->is(Jobeet::slugify('Sensio'), 'sensio'); $t->is(Jobeet::slugify('sensio labs'), 'sensio-labs'); $t->is(Jobeet::slugify('sensio labs'), 'sensio-labs'); $t->is(Jobeet::slugify('paris,france'), 'paris-france'); $t->is(Jobeet::slugify(' sensio'), 'sensio'); $t->is(Jobeet::slugify('sensio '), 'sensio');
Si miras de cerca las pruebas que hemos escrito, notarás que cada linea solo prueba un sola cosa. Esto es algo que necesitas mantener en mente cuando desarrollas pruebas unitarias. Prueba una sola cosa a la vez.
Puedes ahora ejecutar el archivo de pruebas. Si todas pruebas pasan, como esperamos que sea, te alegrará ver una "barra verde". Sino, la infame "barra roja" te alertará que algunas pruebas no pasaron y que necesitas arreglar.
Si una prueba falla, la salida te dará alguna información acerca del porque ésta falló; pero si tienes cientos de pruebas en un archivo, puede ser difícil identificar rápidamente cual falló.
Todos los métodos de prueba lime toman una cadena como su último argumento que sirve como descripción para la prueba. Esto es muy conveniente pues te fuerza a describir que es lo que deseas realmente probar. También te puede servir como una forma de documentación para el comportamiento esperado del método. Vamos agregar algunos mensajes para el archivo de pruebas
slugify
:require_once dirname(__FILE__).'/../bootstrap/unit.php'; $t = new lime_test(6); $t->comment('::slugify()'); $t->is(Jobeet::slugify('Sensio'), 'sensio', '::slugify() converts all characters to lower case'); $t->is(Jobeet::slugify('sensio labs'), 'sensio-labs', '::slugify() replaces a white space by a -'); $t->is(Jobeet::slugify('sensio labs'), 'sensio-labs', '::slugify() replaces several white spaces by a single -'); $t->is(Jobeet::slugify(' sensio'), 'sensio', '::slugify() removes - at the beginning of a string'); $t->is(Jobeet::slugify('sensio '), 'sensio', '::slugify() removes - at the end of a string'); $t->is(Jobeet::slugify('paris,france'), 'paris-france', '::slugify() replaces non-ASCII characters by a -');
La descripción de la prueba es también una herramienta importante cuando tratas de mostrar qué vamos a probar. Puedes ver un patrón en las cadenas de pruebas: ellas son sentencias que describen como el método se comporta y ellas siempre comienzan con el nombre del método a probar.
Cobertura del Código
Al escribir pruebas, es fácil olvidar una porción del código.
Para ayudarte a comprobar que todo el código está bien probado, symfony proporciona la tarea
test:coverage
. Para esta tarea pasale un archivo o directorio test y un archivo o directorio lib un directorio como argumentos y te dirá el porcentaje de código de tu sistema que la prueba cubre:$ php symfony test:coverage test/unit/JobeetTest.php lib/Jobeet.class.php
Si quieres saber que líneas no están cubiertos por tus pruebas, usa la opción
--detailed
:$ php symfony test:coverage --detailed test/unit/JobeetTest.php lib/Jobeet.class.php
Ten en cuenta que cuando la tarea te indica que el código esta completamente probado, simplemente significa que cada línea ha sido ejecutada, no que todos los casos de uso han sido probados.
Como
test:coverage
depende de XDebug
para recoger esta información, necesitas instalarlo y habilitarlo.Agregando Pruebas para las nuevas Características
El slug de una cadena vacía es una cadena vacía. Puedes probarlo, va a funcionar. Pero una cadena vacía en una URL no es que una gran idea. Vamos a cambiar el método
slugify()
para que devuelva la cadena "n-a" en caso de una cadena vacía.
Puedes escribir la prueba primero, entonces actualiza el método, o al revés. Es realmente una cuestión de gusto, pero escribir la prueba primero te da la confianza de que tu código se ajusta en realidad lo que previste:
$t->is(Jobeet::slugify(''), 'n-a', '::slugify() converts the empty string to n-a');
Si lanzas las pruebas ahora, debes obtener una barra roja. Si no es así, significa que la característica ya está implementada o tu prueba no está probando lo que debería estar probando.
Ahora, edita la clase
Jobeet
y añade la siguiente condición al inicio:// lib/Jobeet.class.php static public function slugify($text) { if (empty($text)) { return 'n-a'; } // ... }
La prueba debe pasar ahora según lo esperado, y puedes disfrutar de la barra verde, pero sólo si has recordado actualizar el plan de pruebas. Si no es así, tendrás un mensaje que te dice planeaste seis pruebas y se ejecutó una extra. Después de haber planificado las pruebas debes contar con ellas hasta la fecha y esto es importante, ya que te mantendrá informado si la secuencia de comandos de prueba termina antes de lo deseado.
Agregar Pruebas a causa de un fallo
Digamos que el tiempo ha pasado y uno de tus usuarios informa de un extraño error: algunos vínculos a los puestos de trabajo apuntan a una Página de error 404. Después de algunas investigaciones, vas encontrar que por alguna razón, esos puestos de trabajo no tienen company, position, o location slug. ¿Cómo es posible? Ves a través de los registros en la base de datos y las columnas no están vacías. Lo piensas por un rato, y bingo, encuentras la causa. Cuando una cadena sólo contiene caracteres no ASCII, el método
slugify()
lo convierte a una cadena vacía. Tan feliz de haber encontrado la causa, abres la clase Jobeet
y solucionas el problema de inmediato. Eso es una mala idea. En primer lugar, vamos a añadir una prueba:$t->is(Jobeet::slugify(' - '), 'n-a', '::slugify() converts a string that only contains non-ASCII characters to n-a');
Después de comprobar que la prueba no pasa, edita la clase
Jobeet
y pasa la cadena vacía a comprobar al final del método:static public function slugify($text) { // ... if (empty($text)) { return 'n-a'; } return $text; }
La nueva prueba ahora pasa, al igual que todas los demás. El
slugify()
tenía un error a pesar de nuestra cobertura 100%.
No se puede pensar en todos casos de uso al escribir pruebas, y eso está bien. Pero cuando descubres uno, tienes que escribir una prueba antes de arreglar tu código. También significa que tu código va mejorar con el tiempo, lo que siempre es algo bueno.
Hacia un mejor Método
slugify
Probablemente sabes que Symfony ha sido creado por Franceses, así que vamos a agregar una prueba con una palabra francesa que contiene un "acento":
$t->is(Jobeet::slugify('Développeur Web'), 'developpeur-web', '::slugify() removes accents');
La prueba debe fallar. En lugar de sustituir
é
por e
, el Método slugify()
lo ha sustituido por un guión (-
). Eso es un problema difícil, llamado Transliteración. Esperemos que, si tiene "iconv" instalado, haga el trabajo para nosotros. Reemplaza el código del método slugify
con lo siguiente:// code derived from http://php.vrana.cz/vytvoreni-pratelskeho-url.php static public function slugify($text) { // replace non letter or digits by - $text = preg_replace('#[^\\pL\d]+#u', '-', $text); // trim $text = trim($text, '-'); // transliterate if (function_exists('iconv')) { $text = iconv('utf-8', 'us-ascii//TRANSLIT', $text); } // lowercase $text = strtolower($text); // remove unwanted characters $text = preg_replace('#[^-\w]+#', '', $text); if (empty($text)) { return 'n-a'; } return $text; }
No olvides guardar todos tus archivos PHP con la codificación UTF-8, ya que esta es la codificación por defecto de Symfony, y la utilizada por "iconv" para hacer la Transliteración.
También cambia el archivo de prueba para el funcionamiento de la prueba sólo si "iconv" está disponible:
if (function_exists('iconv')) { $t->is(Jobeet::slugify('Développeur Web'), 'developpeur-web', '::slugify() removes accents'); } else { $t->skip('::slugify() removes accents - iconv not installed'); }
Pruebas Unitarias y Doctrine
Configuración de la Base de datos
Probar en forma unitaria una clase Doctrine del modelo es un poco más complejo ya que requiere una conexión de base de datos. Ya tienes la que utilizas para el desarrollo, pero es un buen hábito crear una base de datos especial para las pruebas.
Durante el día 1, se presentaron los entornos como una forma de variar la configuración de una aplicación. Por defecto, todas las pruebas de symfony se ejecutan en el entorno
test
, así que vamos a configurar una base de datos para el entorno test
:$ php symfony configure:database --name=doctrine --class=sfDoctrineDatabase --env=test "mysql:host=localhost;dbname=jobeet_test" root mYsEcret
La opción
env
le dice a la tarea que la configuración de la base de datos es sólo para el entornotest
. Cuando usamos esta tarea durante el día 3, no pasó ninguna opción env
, por lo que la configuración se aplica a todos los entornos.
Si eres curioso, abre el archivo de configuración
config/databases.yml
para ver como Symfony hace que sea fácil de cambiar la configuración en función del entorno.
Ahora que hemos configurado la base de datos, podemos iniciarla usando la tarea
doctrine:insert-sql
:$ mysqladmin -uroot -pmYsEcret create jobeet_test
$ php symfony doctrine:insert-sql --env=test
Principios de Configuración en Symfony
Durante el día 4, vimos los ajustes procedentes de los archivos de configuración puede ser definido a diferentes niveles.
Estos valores también pueden ser dependientes del entorno. Esto es verdad para la mayoría de los archivos de configuración que hemos utilizado hasta ahora:
databases.yml
, app.yml
, view.yml
, y settings.yml
. En todos los archivos, la clave principal es el entorno, la clave all
está indicando que los ajustes son para todos los entornos:# config/databases.yml dev: doctrine: class: sfDoctrineDatabase test: doctrine: class: sfDoctrineDatabase param: dsn: 'mysql:host=localhost;dbname=jobeet_test' all: doctrine: class: sfDoctrineDatabase param: dsn: 'mysql:host=localhost;dbname=jobeet' username: root password: null
Datos de Prueba
Ahora que ya tenemos una base de datos sólo para pruebas, tenemos que llenarla con datos de prueba. Durante el día 3 aprendimos a utilizar la tarea
doctrine:data-load
, pero en las pruebas es necesario volver a cargar los datos cada vez que ejecutamos las pruebas para conocer el estado inicial de la base de datos.
La tarea
doctrine:data-load
internamente utiliza el método Doctrine_Core::loadData()
para cargar los datos:Doctrine_Core::loadData(sfConfig::get('sf_test_dir').'/fixtures');
El objeto
sfConfig
puede ser utilizado para obtener la ruta completa de un sub-directorio del proyecto. Uso que permite a la estructura de directorio por defecto ser personalizada.
El método
loadData()
toma un directorio o un archivo como primer argumento. También puede tomar un array directorios y/o archivos.
Ya hemos creado algunos datos iniciales en el directorio
data/fixtures/
. Para las pruebas, pondremos los datos en el directorio test/fixtures/
. Estos datos se utilizarán para pruebas unitarias y pruebas funcionales con objetos Doctrine.
Por el momento, copiar los archivos de
data/fixtures/
al directorio test/fixtures/
.
Probando JobeetJob
Vamos a crear algunas de las pruebas unitarias para la clase del modelo,
JobeetJob
.
Como todos nuestros objetos Doctrine harán las pruebas unitarias comenzarán con el mismo código, crea un archivo
Doctrine.php
en el directorio bootstrap/
con el siguiente código:// test/bootstrap/Doctrine.php include(dirname(__FILE__).'/unit.php'); $configuration = ProjectConfiguration::getApplicationConfiguration( 'frontend', 'test', true); new sfDatabaseManager($configuration); Doctrine_Core::loadData(sfConfig::get('sf_test_dir').'/fixtures');
El script se explica bastante por sí mismo:
- Como pasa en todos los controladores frontales, los inicializamos con un objeto de configuración para el entorno
test
:$configuration = ProjectConfiguration::getApplicationConfiguration( 'frontend', 'test', true);
- Creamos un gestor de bases de datos e inicializamos la conexión Doctrine cargando el archivo de configuración
databases.yml
.new sfDatabaseManager($configuration);
- Cargamos nuestros datos de prueba mediante el uso de
Doctrine_Core::loadData()
:Doctrine_Core::loadData(sfConfig::get('sf_test_dir').'/fixtures');
Doctrine se conecta a la base de datos sólo si tiene algunas sentencias SQL para ejecutar.
Ahora que todo está en su lugar, podemos empezar a probar la clase
JobeetJob
.
En primer lugar, tenemos que crear el archivo
JobeetJobTest.php
en test/unit/model
:// test/unit/model/JobeetJobTest.php include(dirname(__FILE__).'/../../bootstrap/Doctrine.php'); $t = new lime_test(1);
Entonces, vamos a empezar por agregar una prueba para el método
getCompanySlug()
:$t->comment('->getCompanySlug()'); $job = Doctrine_Core::getTable('JobeetJob')->createQuery()->fetchOne(); $t->is($job->getCompanySlug(), Jobeet::slugify($job->getCompany()), '->getCompanySlug() return the slug for the company');
Observe que sólo prueba el método
getCompanySlug()
y no si el slug es correcto o no, ya que lo estamos probando a éste en otros lugares.
Escribir pruebas para el método
save()
es ligeramente más complejo:$t->comment('->save()'); $job = create_job(); $job->save(); $expiresAt = date('Y-m-d', time() + 86400 * sfConfig::get('app_active_days')); $t->is($job->getDateTimeObject('expires_at')->format('Y-m-d'), $expiresAt, '->save() updates expires_at if not set'); $job = create_job(array('expires_at' => '2008-08-08')); $job->save(); $t->is($job->getDateTimeObject('expires_at')->format('Y-m-d'), '2008-08-08', '->save() does not update expires_at if set'); function create_job($defaults = array()) { static $category = null; if (is_null($category)) { $category = Doctrine_Core::getTable('JobeetCategory') ->createQuery() ->limit(1) ->fetchOne(); } $job = new JobeetJob(); $job->fromArray(array_merge(array( 'category_id' => $category->getId(), 'company' => 'Sensio Labs', 'position' => 'Senior Tester', 'location' => 'Paris, France', 'description' => 'Testing is fun', 'how_to_apply' => 'Send e-Mail', 'email' => 'job@example.com', 'token' => rand(1111, 9999), 'is_activated' => true, ), $defaults)); return $job; }
Cada vez que añadas pruebas, no te olvides de actualizar el número de pruebas previsto (el plan) en el método constructor
lime_test
. Para el archivoJobeetJobTest
es necesario cambiar de 1
a 3
.Prueba otras Clases Doctrine
Ahora puedes añadir pruebas para todas las demás clases de Doctrine. Como ahora te acostumbraste al proceso de la escritura de pruebas unitarias, debería ser bastante fácil. Comprueba el repositorio para el día de hoy si quieres ver los archivos de datos que hemos creado, y los pruebas unitarias asociadas (bajo la etiqueta
release_day_08
).Set de Pruebas Unitarias
La tarea
test:unit
también se puede utilizar para poner en marcha todas las pruebas unitarias para un proyecto:$ php symfony test:unit
Esta tarea muestra si ha pasado o ha fallado cada uno de los archivos de pruebas:
Si la tarea
test:unit
devuelve un estado "dubious" para un archivo, esto indica que el script se detuvo/murió antes de llegar al final. Ejecutando un archivo de pruebas en forma individual te dará el mensaje de error exacto.Referencia: http://symfony.com/legacy/doc/jobeet/1_4/es/08?orm=Doctrine
Suscribirse a:
Enviar comentarios
(
Atom
)
Sígueme en las Redes Sociales
Donaciones
Datos personales
Entradas populares
-
En este apartado vamos a explicar como ejercutar archivos PHP a través del terminal de Ubuntu. Lo primero que tendríamos que hacer es inst...
-
En este blog voy a comentar un tema que se utilizan en casi todas las páginas web que existen, y es el tema de la paginación. La paginaci...
-
Este post trata de la integración de la librería PHPExcel en Codeigniter, aunque se podría aplicar a cualquier librería, como por ejemplo mP...
-
Ejemplo para añadir o sumar un número determinado de hora/s, minuto/s, segundo/s a una fecha en php. Con la función strtotime se puede ...
-
Este tema es uno de los temas primordiales sobre el framework Codeigniter, ya que en alguna ocación nos hemos visto obligados a recoger dato...
© Espacio Daycry - Espacio de programación 2013 . Powered by Bootstrap , Blogger templates and RWD Testing Tool
No hay comentarios :
Publicar un comentario