Unit Testing: otra manera de testear
Todos los programadores sabemos deberiamos saber que si no probas lo que desarrollas tus usuarios lo harán por vos.
El unit testing (prueba unitaria) es un tipo de testing automático que se hace programando unas rutinas u objetos que controlan si la rutina y/u objeto que programamos funciona como lo esperabamos.
Por ejemplo, una función que nos devuelva la raíz de una función lineal, donde el primer parámetro sería el término lineal y el segundo el término independiente, tendría las siguientes pruebas:
if( funcion_lineal(1, 0) != 0 ){
printf("no funciona!");
}
if( funcion_lineal(2, 1) != -0.5 ){
printf("no funciona!");
}
Este es un ejemplo sencillo, ¿pero cuantos de ustedes se acordaron que si el término lineal es 0 y el independiente es distinto de 0, no tenemos raíz? Ese es otro test que deberíamos tener.
if( funcion_lineal(0, 3) != false ){
printf("no funciona!");
}
Al tener algunos tests podemos programar la rutina y saber si funciona de manera instantánea sin ir probando sus entradas de manera manual, por que ya lo hicimos antes.
Con un código orientado a objetos el código es distinto pero el concepto es el mismo.
Las claves del unit testing son que sean:
- Fácil de hacer
- Automáticos
- Simple de ejecutar (para ejecutarlos continuamente)
Ventajas
¿Y donde están las ventajas de este tipo de testing? Bueno si arreglamos un bug, y desarreglamos otros 2 módulos que funcionaban bien, nos daríamos cuenta de manera inmediata. También se puede usar cuando tenemos un código viejo (legacy code) que sabemos que funciona pero necesitamos cambiarlo sin agregarle bugs (esto es refactoring).
Otra ventaja es que nos ayuda a tener un diseño prolijo de la aplicación ya que si para testear cada módulo (en este caso usamos módulos como conjunto de rutinas u objetos) necesitamos de otros módulos sabemos que la aplicación no es ortogonal, es decir que sus módulos no son independientes.
Además los Unit Testing pueden ser usados como documentación para saber cómo deben usarse los módulos.
Hacer Unit Testing no quiere decir que no haya que hacer beta testing (el testing que conocemos todos). Es imposible que el Unit Testing nos demuestre que el código no tiene errores, sirve para demostrarnos que no tiene esos errores y no los va a volver a tener (a menos que alguien lo rompa y no lo arregle :P).
Desventajas
Una desventaja es la cantidad de código que debemos escribir. En el ejemplo de arriba la funcion sería:
int function function_lineal(int terminoLineal, int terminoIndependiente){
return (-1 * terminoIndependiente) / terminoLineal;
}
Y como vimos antes las pruebas son muchas más que una sola línea. A veces nos puede resultar pesado mantener los tests, no nos olvidemos que cuando haya que cambiar código también tenemos que cambiar los tests.
También tenemos problemas si los métodos que tenemos que ejecutar son muy pesados, si pasar un unit testing completo nos lleva más de 2 horas es muy improbable que los programadores lo hagan seguido.
xUnit
En el ejemplo anterior en realidad parece tedioso hacer tanto código para controlar si funciona bien algo. Y es cierto, es tedioso. Pero existen varios frameworks para muchos lenguajes de programación que hacen que hacer pruebas unitarias sean mucho más sencillo, los llamados xUnit.
xUnit viene de SUnit (desarrollado para SmallTalk) el primer framework desarrollado por Kent Beck (autor del primer paper sobre unit testing) y el más popular JUnit (desarrollado para Java).
Un ejemplo de JUnit siguiendo con el mismo ejemplo anterior pero adaptado a objetos es el siguiente:
public class FuncionesMatematicas extends TestCase
{
protected FuncionesMatematicas funcionesMatematicas;
public void setUp()
{
// inicializamos el objeto, creando su singleton
funcionesMatematicas = FuncionesMatematicas::getInstance();
}
public void testFuncionLineal()
{
// probamos si el metodo devuelve 0
assertEquals ("Raiz Lineal f(x) = x", 0, funcionesMatematicas.getRaizLineal(1, 0));
// probamos si el metodo devuelve -0.5
assertEquals ("Raiz Lineal f(x) = 2x+1", -0.5, funcionesMatematicas.getRaizLineal(2, 1));
// probamos si el metodo devuelve false
assertFalse ("Raiz Lineal sin raiz", funcionesMatematicas.getRaizLineal(0, 3));
}
}
Cada clase es un TestCase y el conjunto de TestCase son TestSuite.
Ya dijimos que es muy importante que sean fáciles de ejecutar, bueno muchos de estos frameworks se ejecutan con una sola llamada en consola o incluso desde nuestro IDE, y nos retorna los assert que pasaron y los que no en cuestión de minutos.
También nos brindan la posibilidad de ejecutar un TestCase, un TestSuite o varios.
Test Driven Development
TDD (Test Driven Development) es una técnica usada en la metodología eXtreme Programming (Programación Extrema) que consiste en programar primero los tests y después lo que estamos testeando (si es así, no estoy delirando). En TDD se sigue un ciclo de desarrollo que consiste en:
- Agregar un test
- Comprobar que falla
- Escribir código para que pase el test (el objetivo es que pase el test no debe estar prolijo ni nada si no se quiere)
- Probar que pasa el test
- Refactoring (ahora si limpiamos el código que habíamos escrito antes)
- Repetir
TDD nos ayuda a crear buenas interfaces de nuestras clases al pensar primero en cómo vamos a llamarlas en vez de cómo funcionarían por dentro. Es muy buena cuando tenemos casos de uso (o similares) donde sabemos bien de antemano qué es lo que tenemos que cumplir.
Mock Objects
Supongamos que tenemos que desarrollar un sistema que lea las RSS de varios portales de noticias y las muestre en un solo sitio y nos diga cuales RSS no han podido ser leídos. Pero, ¿para qué hacer una clase que lee RSS y lo parsea si podemos usar la de Zend Framework (Zend_Feed)?. Solamente nos toca hacer una clase que haga lo siguiente:
class LectorRss{
public function leerRss(array $direccionesRss){
foreach($direccionesRss as $direccionRss){
try{
$rss = Zend_Feed::import($direccionRss);
foreach($rss as $item){
$this->addTitular($item->title, $item->link);
}
$this->addRssCargados($direccionRss);
}catch(Exception $e){
$this->addRssConErrores($direccionRss);
}
}
}
}
Ahora bien, si queremos hacer un test, no podemos controlar cuando un RSS se carga y cuando no. Además tardaría mucho en leer cada feed y procesarlo y (¡volvemos a repetir!) los tests deben ser rápidos (¡sino nadie los va a hacer!, seguimos repitiendo). En esos momentos es donde entran en juego los Mock Objects (para ilustrar el concepto usamos SimpleTest, los otros frameworks no funcionan igual).
class LectorRssTest extends UnitTestCase
{
public function testLeerRss()
{
// Creamos la clase Mock
Mock::generate('Zend_Feed');
$mockZendFeed = new MockZend_Feed();
$mockZendFeed->setReturnValue('import', false);
// La primera vez que se use devolvera $datos
$mockZendFeed->setReturnValue(0, 'import', $datos);
// La segunda devolvera false
$mockZendFeed->setReturnValue(1, 'import', false);
// Esta vez devolvera $otrosDatos
$mockZendFeed->setReturnValue(2, 'import', $otrosDatos);
$lectorRss = new LectorRss();
$lectorRss->leerRss(array("feed1.xml","feed2.xml","feed3.xml"));
$this->assertEqual("Total leidos bien", $lectorRss->getTotalCargados(), 2);
$this->assertEqual("Total con error", $lectorRss->getTotalConErrores(), 1);
}
}
Conclusión
El unit testing es una buena y compleja práctica que nos ayuda a mantener el código prolijo porque nos permite tocar código sin tener miedo a romper algo. En mi caso lo uso siempre que hago una clase o paquete que quiero reutilizar muchas veces (por ejemplo una clase de acceso a Base de Datos), porque puedo justificar el costo de hacerlo y las clases que voy a reutilizar deben tener la mayor calidad posible y la interfaz más fácil para que se use. Si tienen una oportunidad pruebenlo.
Tags: mock objects, tdd, testing, unit testing, xunit

2008-06-12 at 1.58 pm
[…] dejo el Cheat Sheet (a.k.a. Chuleta, a.k.a. machete) de SimpleTest, una clase para hacer Unit Testing en […]