Introducción a las pruebas unitarias en PHP con PHPUnit

Balu27 Enero 2011 - 9:31pm 10 comentarios
Enviar por Email Imprimir

Introducción a las pruebas unitarias en PHP con PHPUnitEsto te suena familiar: Haz venido desarrollando una aplicación durante horas y sientes como si estuvieras yendo en círculos. Corriges un bug y otro aparece. Algunas veces, es el mismo error que encontraste hace 30 minutos, a veces es uno nuevo, pero esta relacionado con el primero. Para la mayoría de desarrolladores, la depuración implica hacer clic en todo el sitio o poner un montón de declaraciones de debugging esperando encontrar un problema.

Han estado en esta situación, ¿cierto? Han tenido algunas frustraciones con sus aplicaciones y se sientan con la espalda hacia atrás y piensan que tiene que haber una mejor manera de desarrollar. Las pruebas de unidad o pruebas unitarias de una aplicación no sólo nos ahorran muchos dolores de cabeza, sino que pueden proporcionarnos un código mucho más fácil de mantener, permitiendo que realicemos cambios más grandes (como refactorizaciones importantes) sin tener dubitaciones...

La clave de las pruebas unitarias es definir lo que entendemos por “unidad”. Una unidad es simplemente un pedazo de funcionalidad que realiza una acción específica, de la cual podemos probar el resultado. Una prueba unitaria, entonces, es una prueba para asegurarnos de que un trozo de funcionalidad hace lo que debería hacer.

Una vez que hemos escrito una serie de pruebas, cada vez que hagamos algún cambio en el código, todo lo que tenemos que hacer es ejecutar el conjunto de pruebas y ver que todo siga igual. De esta manera, podemos asegurarnos de no perder alguna funcionalidad.

Desenmascarando los mitos de las pruebas unitarias

Seguramente estarás sentado allí pensando, “si estas cosas de las pruebas unitarias son tan impresionantes, ¿por qué no todos las aplican en todas sus aplicaciones?” Hay algunas respuestas a esa pregunta, pero ninguna de ellas es lo suficientemente buena. Veamos algunos cuestionamientos comunes, para explicar por qué están lejos de ser razones de peso para evitar escribir pruebas unitarias.

Se tarda demasiado tiempo

Una de las mayores preocupaciones sobre la escritura de pruebas es que termina tomando demasiado tiempo el generarlas. Claro, algunos IDEs permiten autogenerar un conjunto de pruebas básicas, pero sentarse y escribir una buena prueba completa del código, lleva su tiempo. Al igual que muchas de las mejores prácticas en el desarrollo, una pequeña inversión de tiempo para hacer las cosas de la manera correcta puede ahorrarnos mucho tiempo durante la vida útil de un proyecto. Escribir una serie de pruebas sólidas es definitivamente uno de esos casos. Por otra parte, es probable que estés probando cada funcionalidad de tu código visitando tu sitio y dando clic una y otra vez, cada vez que agregas una nueva característica. Ejecutar una conjunto de pruebas predefinidas puede ser mucho más rápido que probar todas las funcionalidades de forma manual.

No hay necesidad de probar: ¡Mi código ya funciona!

Otra afirmación que se suele escuchar de los desarrolladores sobre el conjunto de pruebas es que la aplicación ya funciona, por lo que no hay necesidad real para probarla. Ellos conocen su aplicación y saben que hacer cuando hay que corregir un error en cuestión de segundos. Pero, coloquemos a un desarrollador nuevo en el lugar del anterior, para empezar a ver por qué las pruebas son una buena idea. Sin estas, el novato puede ir cambiando el código sin preocupaciones, y romper quién sabe que. Con una ejecución de pruebas repetibles, algunos de estos errores podrían evitarse.

No es divertido

La última razón de por qué a los desarrolladores no les gustan las pruebas es que simplemente no se divierten escribiéndolas. Los desarrolladores, por naturaleza, quieren resolver problemas. Escribir código es como formar algo de la nada, creando del caos algo útil. Como resultado, ellos ven a las pruebas como algo aburrido, una tarea que puede hacerse uno de estos días, si se tiene el tiempo, después de que el verdadero trabajo se haya hecho. Así pues, ellos dejan de lado las pruebas , incluso durante el desarrollo (¡especialmente durante el desarrollo!) para mantener su proceso constante y limpio. Mirémoslo de esta manera: Nadie piensa que perder horas buscando un error sea divertido y la prueba nos permite poner un poco de esfuerzo por adelantado para evitar la gran frustración de hacer ello.

Un ejemplo

Ahora llegamos a la parte buena: Un ejemplo práctico que te puede picar el diente. Vamos a utilizar una de las herramientas de pruebas unitarias más populares del momento, PHPUnit. Esta sorprendente aplicación fue desarrollada por Sebastian Bergmann y proporciona un excelente conjunto de características para ayudar a probar nuestro código en un instante.

Si todavía no haz instalado PHPUnit, la forma más sencilla es hacerlo es a través de su canal PEAR.

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

Si todo va bien, tendremos todas las herramientas que necesitamos instalar. El instalador de PEAR instalará todas las dependencias que podamos necesitar.

Si quieres instalarlo de forma manual, hay una serie de instrucciones en el manual de PHPUnit. Hay que tener presente, sin embargo, que necesitaremos la instalación de PEAR si queremos utilizar PHPUnit. Se basa en varias librerías de PEAR y, si no tenemos las cosas que necesitamos en su sitio, tendremos algunos errores cuando ejecutemos el binario de PHPUnit. El proceso de instalación manual es un poco más laborioso, pero, créanme, el código que haya sido probado completamente se los agradecerá.

Escribir el primer Caso de Prueba Unitaria

Con PHPUnit, la cosa más básica que escribiremos es un caso de prueba. Un caso de prueba es sólo un término para una clase con varias pruebas distintas, todas relacionadas con la misma funcionalidad. Hay algunas reglas que necesitamos tener presentes a la hora de escribir los casos, a fin de que trabajen con PHPUnit:

  • Frecuentemente, querremos que nuestra clase de prueba extienda la clase PHPUnit_Framework_TestCase. Esto nos dará acceso a ciertas funcionalidades como los métodos para las pruebas setUp() y tearDown().
  • El nombre de la clase de prueba debe imitar el nombre de la clase que se esta probando. Por ejemplo, para probar RemoteConnect, utilizaremos RemoteConnectTest.
  • Al crear métodos de prueba, es necesario que empiecen con la palabra “test”, como testDoesLikeWaffles(). Los métodos deben ser públicos. Podemos tener métodos privados en las pruebas, pero no se podrán ejecutar como pruebas por PHPUnit.
  • Los métodos de prueba no recibirán ningún parámetro. Cuando escribamos las pruebas, es necesario que sean lo más autónomas posibles, tratando de que ellas mismas se necesiten. Esto puede ser frustrante a veces, pero nos proporcionará unas pruebas más limpias y eficaces.

Tenemos que empezar con algunas funciones de prueba, así que aquí tenemos la clase con la que vamos a trabajar en los siguientes ejemplos. Es básica, sólo para mantener las cosas simples. Esto es lo que va en la librería RemoteConnect.php:

<?php
class RemoteConnect
{
  public function
connectToServer($serverName=null)
  {
    if(
$serverName==null){
      throw new
Exception("¡Este no es un nombre de servidor!");
    }
   
$fp = fsockopen($serverName,80);
    return (
$fp) ? true : false;
  }
  public function
returnSampleObject()
  {
    return
$this;
  }
}
?>

Así, por ejemplo, si vamos a probar esta funcionalidad para hacer una petición a un servidor remoto, el test podría tener este aspecto:

<?php
require_once('RemoteConnect.php');
class
RemoteConnectTest extends PHPUnit_Framework_TestCase
{
  public function
setUp(){ }
  public function
tearDown(){ }
  public function
testConnectionIsValid()
  {
   
// prueba para asegurarse de que el objeto de un fsockopen es válido
   
$connObj = new RemoteConnect();
   
$serverName = 'www.google.com';
   
$this->assertTrue($connObj->connectToServer($serverName) !== false);
  }
}
?>

Se podrán percatar de que la clase extiende la clase TestCase de PHPUnit, por lo que una gran cantidad de funcionalidades vienen con ella. Los dos primeros métodos –setUp y tearDown- son ejemplos del tipo de funcionalidad integrada. Son funciones de ayuda que se ejecutan como parte de la prueba normal de funcionamiento. Se ejecutan antes de las pruebas y después de que todo se ha ejecutado, respectivamente. A pesar de su utilidad, no vamos a centrarnos en ellos aún. El objetivo real es el método testConnectionIsValid(). Este método establece el entorno necesario con la creación de una nueva instancia de nuestra clase RemoteConnect y llama al método connectToServer().

Ahora, veamos el verdadero negocio de nuestra prueba. ¿Ves que assertTrue está ahí? Esa es una de las funciones de ayuda de PHPUnit, de los cuales hay unos cuantos. assertTrue es la más simple aserción: lo único que hace es comprobar que una expresión booleana es true. Otras funciones de ayuda pueden poner a prueba las propiedades del objeto, la existencia de archivos, la presencia de una clave dada en una matriz o una coincidencia en una expresión regular, sólo por mencionar algunas. En este caso, queremos estar seguros de que el resultado de connectToServer no sea false, esto quiere decir que nuestra conexión ha fallado por alguna razón.

La ejecución de pruebas

Ejecutar las pruebas es tan sencillo como llamar al ejecutable phpunit y señalar las pruebas. Aquí tenemos un ejemplo de como llamar a nuestra prueba:

phpunit /path/to/tests/RemoteConnectTest.php

Sencillo, ¿cierto? La salida es muy simple: Por cada una de las pruebas del caso de prueba, PHPUnit se ejecuta a través de ellos y recoge algunas estadísticas como el número de pruebas y aserciones. He aquí una vista de la salida de nuestro ejemplo:

PHPUnit 3.4 by Sebastian Bergmann
.
Time: 1 second
Tests: 1, Assertions: 1, Failures 0

Para cada prueba que ejecutamos, veremos un punto (.) si se tiene éxito (como arriba), una “F” si ocurrió un error, y una “I” si la prueba esta marcada como incompleta o una “S” si se ha marcado como Omitida (Skipped).

De forma predeterminada, PHPUnit está configurado para ejecutarse a través de un conjunto de pruebas a la vez e informar las estadísticas totales en un informe sencillo.

Nuestra prueba de ejemplo muestra una prueba de paso –por lo que siempre que proporcionemos los parámetros correctos, nuestro método funcionará como se esperaba. Pero, también necesitamos asegurarnos de poner una prueba cuando las cosas van mal. ¿Qué sucede si el nombre del host que se proporciona al método no existe? ¿El método produce una excepción? Hay que asegurarnos de que a la hora de escribir las pruebas, controlaremos tanto los positivos como los negativos.

En el caso del método connectToServer() de nuestra clase de ejemplo, el proporcionar un nombre de host no válido para la conexión lanzará una excepción. El manejo de excepciones con PHPUnit esta fuera del alcance de este artículo, pero me gustaría recomendar la lectura de la sección correspondiente en la documentación de PHPUnit, si desean profundizar en él.

Hay muchas y diferentes aserciones que pueden ayudarnos a probar los resultados de todos los tipos de llamadas en nuestras aplicaciones. Algunas veces, podemos tener un poco más de creatividad para probar piezas de funcionalidad más complejas; pero las aserciones provistas por PHPUnit cubren la mayoría de los casos que querremos probar. Aquí esta una lista de algunas de las cosas más comunes que podemos encontrar al usarla en nuestras pruebas:

AssertTrue/AssertFalse Comprueba la entrada para verificar si es igual a true/false
AssertEquals Comprueba el resultado frente a otra entrada en busca de coincidencias
AssertGreaterThan Comprueba el resultado para ver si es mayor que un valor (también hay LessThan, GreaterThanOrEqual, y LessThanOrEqual)
AssertContains Comprueba que la entrada contiene un valor específico
AssertType Comprueba que una variable es de un cierto tipo
AssertNull Comprueba que una variable es nula
AssertFileExists Comprueba que un archivo existe
AssertRegExp Comprueba la entrada con una expresión regular

Por ejemplo, digamos que queremos obtener un objeto proveniente de un método (como nuestro método returnSampleObject) y queremos ver si es una instancia de una clase en particular:

<?php
function testIsRightObject() {
 
$connObj = new RemoteConnect();
 
$returnedObject = $connObj->returnSampleObject();
 
$this->assertType('remoteConnect', $returnedObject);
}
?>

Nuestro método fue escrito para devolver la clase en si misma, por lo que esta prueba debería pasar y deberíamos seguir nuestro camino alegremente.

Una aserción por prueba

Al igual que con cualquier área de desarrollo, estas son algunas de las mejores prácticas que valen la pena seguir al escribir pruebas. Una importante es la idea de “una prueba, una aserción”. Esta forma de pensamiento indica que para cada una de las pruebas, sólo puede haber una comprobación o aserción. Nuestros ejemplos de prueba han seguido este principio: Cada prueba sólo llama a un método de aserción. Algunos desarrolladores, sin embargo, piensan que esto puede ser una gran perdida de espacio: “Hey, ya que estamos aquí, vamos a probar esto también”. Este es un ejemplo:

<?php
public function testIsMyString(){
 
$string = "Muy inofensivo";
 
$this->assertGreaterThan(0,strlen($string));
 
$this->assertContains(“42”,$string);
}
?>

Nuestro pequeño ejemplo testIsMyString esta probando dos cosas diferentes. En primer lugar, comprueba que la cadena está vacía (longitud mayor a 0) y luego comprueba que la cadena contiene el número “42”. De inmediato, podemos ver cómo esto se torna difícil: La prueba podría fallar si la cadena fuera, por ejemplo, “cuarenta y dos”. Pero, veríamos exactamente el mismo error si la cadena estuviera vacía, lo cual podría ser causado por un error totalmente diferente. El “error” resultante podría ser engañoso y podría causar cierta confusión en cuanto a lo que realmente esta reportando.

Frameworks que soportan Pruebas de Unidad

Varios de los frameworks más populares basados en PHP (como Zend Framework y Symphony) incluyen la posibilidad de escribir pruebas en contra de su funcionalidad. Debido a que los frameworks MVC implican un poco más de lo que encontraríamos en un simple script PHP o una librería, ellos proveen hooks dentro del framework para ayudarnos a escribir las pruebas.

Podría tener más sentido si vemos un ejemplo. Demos un vistazo a una prueba con Zend Framework, verificando la ruta de una URL a un controlador:

<?php
class CommentControllerTest extends Zend_Test_PHPUnit_ControllerTestCase
{
  public function
setUp()
  {
   
parent::setUp();
  }
  public function
tearDown()
  {
   
parent::tearDown();
  }
  public function
appBootstrap()
  {
   
$this->frontController->registerPlugin(new Initializer('test'));
  }
  public function
testGoHome()
  {
   
$this->dispatch('/home');
   
$this->assertController('home');
  }
}
?>

Por supuesto, este ejemplo es un poco soso, ya que estamos probando una funcionalidad integrada en el framework en lugar de nuestro propio código, pero podemos hacernos una idea. La prueba extiende una clase TestCase distinta, Zend_Test_PHPUnit_ControllerTestCase, para probar un controlador de Zend Framework. También podemos notar, sin embargo, que estamos utilizando PHPUnit. La mayor parte de la prueba nos será familiar, pero tendremos acceso a unas pocas aserciones especiales, como assertController, usada arriba. Podemos encontrar más información en la documentación sobre Zend_Test en el manual de Zend Framework.

Test-Driven Development (Desarrollo Basado en Pruebas)

Sería negligente hablar sobre las pruebas, sin mencionar una técnica que estan cultivando muchos desarrolladores: El desarrollo basado en pruebas. El Test Driven Development o TDD es una técnica usada durante el desarrollo. La idea básica detrás del TDD es que uno debe de escribir las pruebas primero, antes de escribir una sola línea de código de la aplicación. Pero ojo, ¿Cómo sabes que poner en las pruebas si no tienes el código de la aplicación a la vista? Bueno, ese es el punto. En TDD se escribe la prueba para comprobar la funcionalidad de lo previsto y luego se escribe el código para que coincida. Cuando uno inicia y tiene su primer conjunto de pruebas, todas ellas (obviamente) fallan. Al escribir el código de la aplicación, nuestro trabajo estará en luz “verde” y todos los casos de prueba pasarán. Este método nos permite centrarnos más en los primeros requerimiento, en lugar de perdernos en las minucias del código.

Esto puede ser un método difícil de practicar para un novato en pruebas unitarias. Si este es nuestro caso, sería recomendable escribir el código de la aplicación primero, a fin de tener una idea de como realizar las pruebas y usar PHPUnit. Luego, si queremos ir al otro lado, podemos empezar nuestro próximo proyecto con TDD. Hay que tener en cuenta que la primera vez será lento. Afortunadamente, podremos llevar el conocimiento que se obtuvieron en las pruebas y escribir un código que cubra la funcionalidad adecuada.

En resumen

Espero que hayamos tenido una buena introducción al mundo de las pruebas unitarias. A pesar de que hay una multitud de temas que no se han tocado, he tratado de escribir un buen punto de partida, desde donde podemos empezar a escribir nuestras propias pruebas.

Vía | sitepoint

Comentarios

Imagen de joaquin
joaquin

Hola.

Ya he utilizado phpUnit para probar funcionalidades de clases y desarrollos muy marcados pero, ¿Existe alguna forma de probar el funcionamiento de un formulario?

Por ejemplo: Imaginemos que tenemos un formulario de suscripción con varios pasos.

A mi me interesa saber que si relleno todos los campos correctamente en el primer paso y voy al segudno, relleno los datos de pago y el proceso de pago me devuelve OK, quiero saber si se da de alta el usuario en mi base de datos y todas las validaciones que pueda necesitar.

¿Es esto posible?

Si esto se pudiera hacer sería muy interesante, ya que de este modo se podrían probar muchas funcionalidades de forma muy rápida y minimizando los tests manuales.

Imagen de baluart
baluart

Claro que se puede, pero tendrías que hacer una prueba por cada paso o funcionalidad del sistema de pagos.

Imagen de Hector Benitez
Hector Benitez

Para esto, probablemente te servira mas realizar pruebas funcionales en lugar de pruebas de unidad, Selenium es una excelente herramienta, las pruebas funcionales abren un browser y realizan tareas de forma automatizada como rellenar formularios o navegar dentro de tu sitio, saludos.

Imagen de Cristian Mamani
Cristian Mamani

Buena introduccion y bien explicado todo.

Imagen de Ivan87
Ivan87

Hola.

Me parece perfecto tu post, entendí muy bien como funciona phpunit en casos sencillos, pero que pasa si tengo una clase A que hereda de otra clase B? en php no existe herencia múltiple, así que o heredo el framework phpunit o la clase B que necesita mi clase A, como puedo implementar un test para la clase A?

Saludos.

Imagen de Irwin
Irwin

Excelente amigo ;)

Imagen de Pablo Monroy
Pablo Monroy

Excelente. Me ha servido mucho para comenzar a usar pruebas unitarias.

Imagen de camilo andres
camilo andres

solo para comentar que "assertType" ya no es un metodo (de phpunit v3), un reemplazo para el ejercicio podrías er "assertInstanceOf".

Imagen de Armando.hdez

La información excelente, explicas lo básico de una manera muy clara. Gracias.

Imagen de Hector Benitez
Hector Benitez

Muy buen post, solo como comentario, la instalación usando pear ya no tiene soporte, ellos mismos te recomiendan usar composer o el archivo .phar, seria muy util que actualizaras esa parte de tu post, saludos

Tutoriales

Cómo descargar videos de VK.com
En este artículo voy a explicar como descargar videos y películas...
Descargar Facebook Móvil Gratis
Por si aún no lo han hecho, es posible descargar Facebook Móvil...
Cómo generar tráfico web con las redes sociales - Paso a Paso
Muchas empresas están publicando contenidos como la forma de crear...

Artículo Recomendado

3 Tips cruciales para recuperar archivos eliminados
¿Te imaginas perder el trabajo de toda una semana en tan solo unos segundos? Todos hemos pasado por este problema. Quizás eliminamos por error un archivo importante o lo borramos sin pensar que era valioso para otro... más