![]() |
|
The Askeet TutorialDía quince del calendario de symfony: Pruebas unitarias |
WARNING: The SVN source code found in the release_day tags is outdated. Please refer to the current version until each day code is updated.
You are currently reading "The Askeet Tutorial" which is licensed under the Creative Commons Attribution-Noncommercial-No Derivative Works 3.0 Unported License license.
sfTestBrowser
WebTestCase
![]() |
This work is licensed under a
Creative Commons Attribution-Noncommercial-No Derivative Works 3.0 Unported License.
Translation of this work into another language is explicitly allowed. |
Ahora las preguntas están bien organizadas en la web de askeet, gracias a la característica de etiquetado para la comunidad que añadimos ayer.
Pero hay una cosa que no ha sido descrita hasta ahora, a pesar de su importancia en el ciclo de vida de las aplicaciones web. Las Pruebas unitarias son uno de los mayores avances en la programación desde la orientación a objetos. Permiten un proceso de desarrollo seguro, refactorizar sin miedo, y pueden a veces reemplazar la documentación ya que ilustran claramente lo que se supone que debe hacer la aplicación. Symfony permite y recomienda las pruebas unitarias, y provee herramientas para ellas. La descripción de estas herramientas - y la adición de unas pocas pruebas unitarias para probar askeet - nos llevarán la mayor parte de nuestro tiempo de hoy.
Hay muchos frameworks de pruebas unitarias en el mundo de PHP, la mayoría basadas en Junit. Nosotros no desarrollamos otra más para symfony, pero sin embargo integramos la más maduras de todas ellas, Simple Test. Es estable, bien documentada, y ofreces montones de características que son de considerable valor para todos lo proyectos PHP, incluyendo los de symfony. Si no aún no la conoces, quedas avisado para buscar su documentación, que es muy clara y progresiva.
Simple Test no viene empaquetado con symfony, pero es muy fácil de instalar. Lo primero, descarga el archivo PEAR instalable de Simple Test desde SourceForge. Instálalo vía pear llamando a:
$ pear install simpletest_1.0.0.tgz
Si quieres escribir un batch script que use la librería de Simple Test, lo único que tienes que hacer es insertar estas pocas líneas de código al principio del script:
<?php require_once('simpletest/unit_tester.php'); require_once('simpletest/reporter.php'); ?>
Symfony hace esto por ti si usas la línea de comandos del test; pronto hablaremos sobre esto.
Debido a los cambios en PHP 5.0.5 que nos son compatibles con versiones anteriores, actualmente Simple Test no funciona si tienes una versión de PHP superior a la 5.0.4. Esto cambiará en breve (hay disponible una versión alfa que trata este problema), pero desafortunadamente es probable que el resto de este tutorial no funcione si tienes una versión posterior.
Cada proyecto symfony tiene un directorio test/
, dividido en subdirectorios por cada aplicación. Para askeet, si buscas en el directorio askeet/test/functional/frontend/
, verás que ya hay unos pocos archivos:
answerActionsTest.php
feedActionsTest.php
mailActionsTest.php
sidebarActionsTest.php
userActionsTest.php
Todos contienen el mismo código inicial:
<?php class answerActionsWebBrowserTest extends UnitTestCase { private $browser = null; public function setUp () { // create a new test browser $this->browser = new sfTestBrowser(); $this->browser->initialize('hostname'); } public function tearDown () { $this->browser->shutdown(); } public function test_simple() { $url = '/answer/index'; $html = $this->browser->get($url); $this->assertWantedPattern('/answer/', $html); } } ?>
La clase UnitTestCase
es la clase núcleo de las pruebas unitarias de Simple Test. El método setUp()
se ejecuta justo antes de cada método de la prueba, y tearDown()
se ejecuta justo después de cada método de la prueba. Los métodos
reales de la prueba comienzan con la palabra 'test'. Para comprobar si
un trozo de código se está comportando como esperas, se usa una
aserción, que es un método que comprueba que algo es cierto. En Simple
Test, las aserciones comienzan con assert
. En este
ejemplo, se implementa una prueba unitaria, y busca por la palabra
'user' en la página por defecto del módulo. Este archivo autogenerado
es una pequeña ayuda para que comiences.
De hecho, cada vez que llamas a symfony init-module
, symfony crea un esqueleto como este en el directorio test/[appname]/
para
almacenar las pruebas unitarias asociadas al módulo creado. El problema
es que tan pronto como modifiques la plantilla por defecto, los test
creados para ayudarte serán pasados nunca más (estos comprueban que el
título de la página por defecto, que es 'module $modulename'). Así que
por ahora, borraremos esos archivos y trabajaremos en nuestros propios
casos de prueba.
Durante el día 13, creamos un archivo Tag.class.php
con dos funciones dedicadas a la manipulación de etiquetas. Añadiremos
unas pocas pruebas unitarias para nuestra librería de etiquetas.
Crea el archivo TagTest.php
(todos los archivos de casos de prueba deben terminar en Test
para que Simple Test los encuentre):
<?php require_once('Tag.class.php'); class TagTest extends UnitTestCase { public function test_normalize() { $tests = array( 'FOO' => 'foo', ' foo' => 'foo', 'foo ' => 'foo', ' foo ' => 'foo', 'foo-bar' => 'foobar', ); foreach ($tests as $tag => $normalized_tag) { $this->assertEqual($normalized_tag, Tag::normalize($tag)); } } } ?>
El primer caso de prueba que implementaremos concierne al método Tag::normalize()
.
Las pruebas unitarias están pensadas para probar un caso cada vez, así
que descomponemos el resultado esperado del método del texto en casos
básicos. Sabemos que el método Tag::normalize()
debería
devolver una versión en minúsculas de su argumento, sin espacios -
tanto antes como después del argumento - y sin ningún carácter
especial. Los cinco casos de prueba en el array $test
son suficientes para probar esto.
Para cada uno de los casos de prueba básicos, comparamos la versión
normalizada de la entrada con el resultado esperado, con una llamada al
método ->assertEqual()
. Esto es el corazón de una
prueba unitaria. Si falla, el nombre del caso de prueba será mostrado
cuando el conjunto de pruebas sea ejecutado. Si lo pasa, simplemente se
añadirá al número de pruebas pasadas.
Podríamos añadir una última prueba con la palabra ' FOo-bar '
,
pero esto mezcla casos básicos. Si este caso falla , no tendrás una
idea clara de la causa precisa del problema, y necesitarás investigar
más. Mantener los casos básicos te da la seguridad de que el error será
fácilmente localizado.
Nota: La extensa lista de métodos
assert
puede ser encontrada en la documentación de Simple Test.
La línea de comandos de symfony permite ejecutar todas las pruebas de una vez con un único comando (recuerda llamarlo desde el directorio raíz de tu proyecto):
$ symfony test-functional frontend
Al llamar a este comando se ejecutan todas las pruebas del directorio test/functional/frontend/
, y por ahora estas son solo algunas de nuestro nuevo conjunto TagTest.php
. Estas pruebas serán pasadas y la línea de comandos mostrará:
$ symfony test-functional frontend
Test suite in (test/frontend)
OK
Test cases run: 1/1, Passes: 5, Failures: 0, Exceptions: 0
Nota: Las pruebas lanzadas por la línea de comandos de symfony no necesitan incluir la librería Simple Test (
unit_tester.php
yreporter.php
son incluídas automáticamente).
El mayor beneficio de las pruebas unitarias se aprecia cuando se hace desarrollo basado en pruebas. En esta metodología, las pruebas son escritas antes de escribir la función.
Con el ejemplo de arriba, escribirías un método vacío Tag::normalize()
,
luego escribirías el primer caso de prueba ('Foo'/'foo'), y luego
ejecutarías el conjunto de pruebas. La prueba fallaría. Entonces
añadirías el código necesario para convertir el argumento en minúsculas
y devolverlo en el método Tag::normalize()
, luego ejecutas la prueba de nuevo. Esta vez la prueba se pasaría la prueba.
Así pues añadirías las pruebas para espacios en blanco, las ejecutarías, verías que fallan, añadirías el código necesario para eliminar los espacios, ejecutarías las pruebas de nuevo, verías que se pasan. Entonces harías lo mismo para los caracteres especiales.
Escribir primero las pruebas ayuda para enfocarte en las cosas que la función debería hacer antes de desarrollarla realmente. Ésta es una buena práctica tal como otras metodologías, como Programación Extrema, que también es recomendable. Además ten en cuenta el innegable hecho de que si no escribes las pruebas primero, nunca las escribirás.
Una última recomendación: mantén tus conjuntos de pruebas tan simples como el descrito aquí. Una aplicación construida con la metodología basada en pruebas termina aproximadamente con tanto código de pruebas como código real, así que si no quieres perder tiempo depurando tus casos de prueba...
Ahora añadiremos las pruebas para comprobar el segundo método del objeto Tag
, el cual divide una cadena compuesta de varias etiquetas en un array de etiquetas. Añade el siguiente método a la clase TagTest
:
public function test_splitPhrase() { $tests = array( 'foo' => array('foo'), 'foo bar' => array('foo', 'bar'), ' foo bar ' => array('foo', 'bar'), '"foo bar" askeet' => array('foo bar', 'askeet'), "'foo bar' askeet" => array('foo bar', 'askeet'), ); foreach ($tests as $tag => $tags) { $this->assertEqual($tags, Tag::splitPhrase($tag)); } }
Nota: Como buena práctica, recomendamos nombrar los archivos de las pruebas fuera de las clases que deben probar, y los casos de prueba fuera de los métodos que deben probar. Pronto tu directorio
test/
contendrá un montón de archivos, y encontrar una prueba podría resultar difícil a la larga si no lo haces.
Si intentas ejecutar las pruebas de nuevo, fallan:
$ symfony test-functional frontend
Test suite in (test/frontend)
1) Equal expectation fails as key list [0, 1] does not match key list [0, 1, 2] at line [35]
in test_splitPhrase
in TagTest
in /home/production/askeet/test/functional/frontend/TagTest.php
FAILURES!!!
Test cases run: 1/1, Passes: 9, Failures: 1, Exceptions: 0
De acuerdo, uno de los casos de prueba de test_splitPhrase
falla. Para encontrar cuál, necesitarás quitarlos uno por uno para ver
cuando pasa la prueba. En este caso, es el último, cuando probamos el
manejo de las comillas simples. El método Tag::splitPhrase()
actual no traduce adecuadamente esta cadena. Como parte de tu tarea, tendrás que corregirlo para mañana.
Esto ilustra el hecho de que si apilas demasiados casos de prueba básicos en un array, es difícil localizar un fallo. Siempre es preferible partir los casos de prueba largos en métodos, ya que Simple Test muestra el nombre del método donde falló la prueba.
Las aplicaciones web no son solo objetos que se comportan más o menos como funciones. El complejo mecanismo de la petición de una página, el HTML resultado y las interacciones del navegador requieren más de lo que se ha mostrado antes para construir un completo conjunto de pruebas unitarias para una aplicación web de symfony.
Examinaremos tres formas diferentes de implementar una sencilla prueba de una aplicación web. La prueba tiene que hacer una petición para ver el detalle de la primera pregunta, y supone que algún texto de la respuesta está presente.
sfTestBrowser
Symfony proporciona un objeto llamado sfTestBrowser
,
que permite simular la navegación si un navegador y, más importante,
sin un servidor web. Su situación dentro del framework permite a este
objeto evitar completamente la capa de transporte http. Esto significa
que la navegación simulada por el sfTestBrowser
es rápida, e independiente de la configuración del servidor. ya que no lo usa.
Veamos como hacer una petición de una página con este objeto:
$browser = new sfTestBrowser(); $browser->initialize(); $html = $browser->get('uri'); // do some test on $html $browser->shutdown();
La petición get()
tiene una URI enrutada como parámetro
(no una URI interna), y devuelve un página HTML en crudo (una cadena de
texto). Así puedes proceder a hacer todo tipo de pruebas a esta página,
usando el método assert*()
del objeto UnitTestCase
.
Puedes pasar parámetros a tus llamadas como harías en la barra de URL de tu navegador:
$html = $browser->get('/frontend_test.php/question/what-can-i-offer-to-my-stepmother');
La razón por la que usamos una controlador frontal específico (frontend_test.php
) se explicará en la próxima sección.
El sfTestBrowser
simula una cookie. Esto significa que con un simple objeto sfTestBrowser
,
puedes pedir varias páginas una tras otra, y serán consideradas parte
de una única sesión por el framework. Además, el hecho de que sfTestBrowser
use URIs enrutadas en vez de URIs internas te permite probar el sistema de enrutamiento.
Para implementar nuestra prueba de la web, el método test_QuestionShow()
debe ser construido así:
<?php class QuestionTest extends UnitTestCase { public function test_QuestionShow() { $browser = new sfTestBrowser(); $browser->initialize(); $html = $browser->get('frontend_test.php/question/what-can-i-offer-to-my-step-mother'); $this->assertWantedPattern('/My stepmother has everything a stepmother is usually offered/', $html); $browser->shutdown(); } }
Ya que casi todos las pruebas unitarias de la web necesitarán inicializar un nuevo sfTestBrowser
y cerrarlo después del test, sería mejor que movieras parte del código a los métodos ->setUp()
y ->tearDown()
:
<?php class QuestionTest extends UnitTestCase { private $browser = null; public function setUp() { $this->browser = new sfTestBrowser(); $this->browser->initialize(); } public function tearDown() { $this->browser->shutdown(); } public function test_QuestionShow() { $html = $this->browser->get('frontend_test.php/question/what-can-i-offer-to-my-step-mother'); $this->assertWantedPattern('/My stepmother has everything a stepmother is usually offered/', $html); } }
Ahora, cada nuevo método test
que añadas tendrá un objeto sfTestBrowser
limpio para empezar con él. Aquí puedes reconocer los casos de prueba auto-generados mencionados al principio de este tutorial.
WebTestCase
Simple Test viene con una clase WebTestCase
, que
incluye facilidades para la navegación, comprueba contenido y cookies,
y manejo de formularios. Las pruebas amplían esta clase permitiéndote
simular una sesión de navegación con la capa de transporte http. De
nuevo, la documentación de Simple Test explica en detalle cómo usar esta clase.
Las pruebas construidas con WebTestCase
son más lentas que las construidas con sfTestBrowser
,
ya que el servidor web está en medio de cada petición. También
requieren que tengas una configuración del servidor web funcionando.
Sin embargo, el objeto WebTestCase
viene con mucho métodos de navegación con assert*()
como principal. Usando estos métodos, puedes simular una sesión de
navegación compleja, Aquí está el subconjunto de los métodos de
navegación de WebTestCase
:
- | - | - |
---|---|---|
get($url, $parameters) |
setField($name, $value) |
authenticate($name, $password) |
post($url, $parameters) |
clickSubmit($label) |
restart() |
back() |
clickImage($label, $x, $y) |
getCookie($name) |
forward() |
clickLink($label, $index) |
ageCookies($interval) |
Fácilmente podríamos hacer el mismo caso de prueba con un WebTestCase
. Ten cuidado ya que ahora necesitas introducir las URIs completas, ya que serán pedidas por el servidor web:
require_once('simpletest/web_tester.php'); class QuestionTest extends WebTestCase { public function test_QuestionShow() { $this->get('http://askeet/frontend_test.php/question/what-can-i-offer-to-my-step-mother'); $this->assertWantedPattern('/My stepmother has everything a stepmother is usually offered/'); } }
Los métodos adicionales de este objeto podrían ayudarnos a probar cómo se maneja un formulario enviado, por ejemplo para la prueba unitaria del proceso de identificación:
public function test_QuestionAdd() { $this->get('http://askeet/frontend_dev.php/'); $this->assertLink('sign in/register'); $this->clickLink('sign in/register'); $this->assertWantedPattern('/nickname:/'); $this->setField('nickname', 'fabpot'); $this->setField('password', 'symfony'); $this->clickSubmit('sign in'); $this->assertWantedPattern('/fabpot profile/'); }
Esto es muy práctico para permitir establecer un valor a los campos
y enviar el formulario tal como harías a mano. Si tuvieras que simular
esto haciendo una petición POST
(esto es posible mediante una llamada a ->post($uri, $parameters)
),
deberías escribir en la función de prueba el objetivo de la acción y
todos los campos ocultos, así dependería demasiado de la
implementación. Para más información sobre las pruebas de formularios
con Simple Test, lee el capítulo relacionado de la documentación de Simple Test.
El principal inconveniente de las pruebas de sfTestBrowser
y WebTestCase
es que no pueden simular JavaScript. Para interacciones más complejas,
como interacciones AJAX por ejemplo, necesitas la posibilidad de
reproducir exactamente lo que haría un usuario con el ratón y el
teclado. Normalmente, estas pruebas se hacen a mano, pero consumen
mucho tiempo y son propensas a errores.
La solución, esta vez, viene del mundo de JavaScript. Se llama Selenium y es mejor cuando se usa con la extensión para Firefox Selenium Recorder. Selenium ejecuta un conjunto de acciones en una página tal como lo haría un usuario normal, usando la ventana del navegador.
Selenium no viene empaquetado con symfony por defecto. Para instalarlo, necesitas crear un nuevo directorio selenium/
en tu directorio web/
, y desempaquetar allí el contenido del archivo Selenium.
Esto es debido a que Selenium depende de JavaScript, y las opciones de
seguridad estándar en la mayoría de los navegadores no permitirían
ejecutarlo a menos que esté disponible en el mismo host y puerto que tu
aplicación.
Nota: Ten cuidado de no transferir el directorio
selenium/
a tu host de producción, ya que estaría accesible desde el exterior.
Las pruebas de Selenium se escriben en HTML y se guardan en el directorio selenium/tests/
. Por ejemplo, para hacer la prueba unitaria simple sobre el detalle de una pregunta, crea el siguiente archivo llamado testQuestion.html
:
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <head> <meta content="text/html; charset=UTF-8" http-equiv="content-type"> <title>Question tests</title> </head> <body> <table cellspacing="0"> <tbody> <tr><td colspan="3">First step</td></tr> <tr> <td>open</td> <td>/frontend_test.php/</td> <td> </td> </tr> <tr> <td>clickAndWait</td> <td>link=What can I offer to my step mother?</td> <td> </td> </tr> <tr> <td>assertTextPresent</td> <td>My stepmother has everything a stepmother is usually offered</td> <td> </td> </tr> </tbody> </table> </body> </html>
Un caso de prueba se representa en un documento HTML, conteniendo
una tabla con 3 columnas: comando, objetivo, valor. No todos los
comandos tienen valor, sin embargo. En este caso dejamos la columna en
blanco o usamos
para que la tabla luzca mejor.
También necesitas añadir esta prueba a la suite de pruebas global insertando una nueva línea en la tabla del archivo TestSuite.html
, situado en el mismo directorio:
...
<tr><td><a href='./testQuestion.html'>My First Test</a></td></tr>
...
Para ejecutar la prueba, simplemente navega hasta
http://askeet/selenium/index.html
Selecciona 'Main Test Suite', pincha en el botón para ejecutar todos los tests, y observa cómo tu navegador reproduce los pasos que le has dicho que haga.
Nota: Como las pruebas de Selenium se ejecutan en un navegador real, también permiten probar inconsistencias de navegación. Construye pruebas con un navegador, y pruébalas en todos los otros navegadores en los que se supone que debe funcionar tu sitio con una simple petición.
El hecho de que las pruebas de Selenium estén escritas en HTML podría convertir la escritura de las pruebas en una molestia. Pero gracias a la extensión de Firefox de Selenium, todo lo que se necesita para crear pruebas es ejecutar la prueba una vez en una sesión de grabación. Mientras navegas en una sesión de grabación, puedes añadir pruebas de tipo aserción haciendo clic con el botón derecho en la ventana del navegador y marcando la casilla correspondiente bajo el Appende Selenium Command en el menú desplegado.
Por ejemplo, la siguiente prueba de Selenium comprueba la puntuación AJAX de una pregunta. El usuario 'fabpot' se identifica, muestra la segunda página de preguntas para acceder a la única en la que no se ha interesado, entonces pincha en el enlace 'interested?', y comprueba que cambia el '?' por '!'. Esto será grabado con la extensión de Firefox, y lleva menos de 30 segundos:
<html> <head><title>New Test</title></head> <body> <table cellpadding="1" cellspacing="1" border="1"> <thead> <tr><td rowspan="1" colspan="3">New Test</td></tr> </thead><tbody> <tr> <td>open</td> <td>/frontend_dev.php/</td> <td></td> </tr> <tr> <td>clickAndWait</td> <td>link=sign in/register</td> <td></td> </tr> <tr> <td>type</td> <td>//div/input[@value="" and @id="nickname" and @name="nickname"]</td> <td>fabpot</td> </tr> <tr> <td>type</td> <td>//div/input[@value="" and @id="password" and @name="password"]</td> <td>symfony</td> </tr> <tr> <td>clickAndWait</td> <td>//input[@type='submit' and @value='sign in']</td> <td></td> </tr> <tr> <td>clickAndWait</td> <td>link=2</td> <td></td> </tr> <tr> <td>click</td> <td>link=interested?</td> <td></td> </tr> <tr> <td>pause</td> <td>3000</td> <td></td> </tr> <tr> <td>verifyTextPresent</td> <td>interested!</td> <td></td> </tr> <tr> <td>clickAndWait</td> <td>link=sign out</td> <td></td> </tr> </tbody></table> </body> </html>
No olvides reiniciar los datos de prueba (llamando a php batch/load_data.php
) antes de lanzar las pruebas de Selenium.
Nota: Tenemos que añadir una acción
pause
después de pinchar en el enlace AJAX, ya que Selenium no debería sgurio adelante con la prueba. Esto es una viso general para las pruebas de interacciones AJAX con Selenium.
Puedes salvar la prueba en un archivo HTML para construir una Suite de Pruebas para tu aplicación. La extensión de Firefox te permite incluso ejecutar las pruebas de Selenium que has grabado con ella.
Las pruebas de webs tienen que usar un controlador frontal, y pueden
usar un entorno específico (es decir, una configuración). Symfony
proporciona un entorno test
por defecto para todas las
aplicaciones, especialmente para las pruebas unitarias. Puedes definir
un conjunto personalizado de opciones para esto en el directorio config/
de tu aplicación. Los parámetros de configuración por defecto son (extraídos de askeet/apps/frontend/config/settings.yml
):
test:
.settings:
# E_ALL | E_STRICT & ~E_NOTICE = 2047
error_reporting: 2047
cache: off
stats: off
web_debug: off
La caché, las estadísticas y la barra de herramientas de depuración
web se desactivan. Sin embargo, la ejecución del código aún deja trazas
en el archivo de log (askeet/log/frontend_test.log
).
Puedes tener una configuración de conexión para una base de datos
específica, por ejemplo para usar otra base de datos con los datos de
prueba.
Esto es por lo que todas las URIs externas mencionadas anteriormente muestran frontend_test.php
: el controlador frontal del test
tiene que especificarse - de otro modo, se usará el controlador de producción index.php
en su lugar, y no deberías permitir usar diferentes bases de datos o tener que separa logs para tus pruebas unitarias.
Nota: se presupone que las pruebas web no se ejecutarán en producción. Son una herramienta de desarrollo, y como tal, deberían ejecutarse en el ordenador del desarrollador, no en el servidor de host.
De momento no hay una solución ideal para las pruebas unitarias de las aplicaciones PHP hechas con symfony. Cada una de las tres soluciones presentadas hoy tienen grandes ventajas, pero si tienes un amplío acercamiento de las pruebas unitarias, probablemente necesitarás usar las tres. Para askeet, las pruebas unitarias serán añadidas poco a poco en el código SVN. Compruébalo de vez en cuando, o propónte tú mismo aumentar la solidez de la aplicación.
Las pruebas unitarias también pueden ser usadas para evitar la regresión. Refactorizar un método puede crear nuevos errores que no aparecían antes. Por esto es por lo que también es una buena práctica ejecutar todas las pruebas unitarias antes de desplegar una nueva versión de una aplicación en producción - esto se llama pruebas de regresión. Hablaremos más sobre esto cuando entremos en la utilización de la aplicación
Mañana... bueno, mañana será otro día. Si tienes alguna pregunta sobre el tutorial de hoy, no te cohibas de preguntárnosla en el foro de askeet.
If you find a typo or an error, please register and open a ticket.
If you need support or have a technical question, please post to the user mailing-list or to the forum.