Feb 8, 2011

Desarrollo guiado por pruebas - Remix

Hace poco más de dos años escribí un artículo para una revista cubana de telecomunicaciones e informática que nunca fue publicado. Después de esto decidí publicarlo en Google Knol, que por aquellos tiempos prometía mucho. Resulta ser que posiblemente el 90% de ustedes jamás hayan oído hablar de Knol, porque su existencia ha sido tan poco conocida como su capacidad de evolución. Resultado final: el artículo quedó olvidado en un rincón de Internet donde nadie jamás va a buscar nada.

A pesar que es de hace dos años (noviembre 2008), su contenido mantiene total vigencia, así que no hay mejor momento que este para re-publicarlo en el blog después de hacerle pequeños cambios. Además, viene como anillo al dedo después que fuera acusado por mi aversión a hacer pruebas unitarias producto del artículo "¿Qué pasa con mis pruebas unitarias?". Espero que el texto a continuación despeje todas las dudas sobre mi posición al respecto.

Sube el Telón

Software: una palabra que desde hace ya un tiempo ha pasado a formar parte muy importante en la vida de muchas personas a lo largo de todo el mundo. Unos, dependen de él para el cumplimiento normal de sus actividades diarias. Otros, lo tienen como su principal herramienta de trabajo. Algunos, un poco más dichosos, son los responsables de su creación y mantenimiento: hacen el papel de Dios como Creador, o de Madre Naturaleza, no importa como lo prefieran.

La creación de software es una actividad que desde los mismos inicios de la Era Digital ha reclutado en sus filas a miles y miles de personas que persiguen en su tarea un único objetivo: la perfección. Muchas páginas han sido escritas tratando de dar un poco de luz en el camino para lograr programas estables, flexibles, robustos y, nuevamente, perfectos.

Tanto tiempo y – parece mentira – el hombre no ha logrado aún su principal anhelo: conjugar experiencia, destreza y conocimientos para construir un sistema – entiéndase sistema como software – libre de imperfecciones. Un sueño que ha resultado ser un paradigma prácticamente inalcanzable.

Todo creador – o programador, para ir entrando en términos técnicos – respondería tajantemente a la pregunta "¿Tiene problemas tu sistema?" con una rotunda negativa. Basta con modificar dos puntos: uno, el entrevistado; dos, el pronombre posesivo de la interrogante y formularle a un cliente del sistema "¿Tiene problemas su sistema?". Es muy probable que la lista resulte interminable y hasta irrisoria teniendo en cuenta la primera respuesta. Por alguna misteriosa razón, programador y cliente – o usuario, si lo prefiere – alimentan un volcán de diferencias y contradicciones; una batalla ancestral que nadie apoya y todos tenemos.

He tenido la suerte – o la desgracia – de vivir ambas vidas como otros tantos. He tenido respuestas, o mejor dicho, he tenido que responder las dos preguntas de la discordia. Cada una desde un punto de vista diferente, y – paradójicamente –, dando respuestas muy poco diferentes a las anteriores.

Esta posibilidad se ha convertido en inspiración y catalizador para dedicar gran parte de mi tiempo al estudio de diversas prácticas que me permitan acercarme cada vez más al cliente. Todos sabemos que la perfección, o no existe, o es demasiado costosa obtenerla; sin embargo, podemos lograr un producto final que se acerque lo suficiente como para cerrar en cierto modo la brecha que separa ambos mundos.

Primer Acto

De los cientos de técnicas que persiguen nuestro objetivo quisiera referirme no a la más moderna, sino a una de las más poderosas: el Test, o como diríamos los hispano parlantes, la Prueba. Seguramente una palabra tan oída como "calidad" nos haga pensar precisamente en este punto: el producto una vez concluido necesita pasar por una etapa de pruebas hasta tanto su calidad sea certificada.

En todo equipo de desarrollo, desde que se habla de la producción de software como ciencia, existe – o al menos debiera existir – un grupo de profesionales encargados de realizar las pruebas pertinentes al producto en desarrollo, de forma tal que el resultado final se acerque a los parámetros de calidad establecidos o deseados, en dependencia del caso.

Esto no es nuevo en ningún lugar del mundo, ni siquiera en aquellos donde la producción de software es, si no una técnica de último momento, sí una tarea que hace muy poco tiempo es ha comenzado a cobrar auge rápida y firmemente. La técnica de realizar pruebas al producto – en nuestro caso, al software – no tiene nada de novedoso, pero si variamos su forma de aplicación, entonces nos encontraremos en un camino prácticamente inexplorado por cientos de equipos de desarrollo.

Más explícitamente me encuentro hablando de TDD o Test-Driven Development – que en nuestro idioma sería algo así como "Desarrollo Guiado por Pruebas". Lo más sorprendente del TDD es que no incluye prácticamente ninguna idea nueva, sino que combina las prácticas existentes desde el mismo surgimiento de programas y programadores para garantizar un código limpio que funcione correctamente.

El TDD es una disciplina de la cual pueden encontrarse cientos de documentos publicados con solo realizar una consulta en cualquiera de los buscadores de Internet. Digo disciplina y me refiero estrictamente al sentido directo de la palabra: la utilización de TDD requiere por parte de los programadores de la aplicación de un conjunto de prácticas de la forma, dónde y cuándo se requieran. No hay espacios para la improvisación o descontrol. TDD requiere de esfuerzo, conciencia, y – nuevamente – disciplina.

Para comenzar a formar parte de este mundo, un concepto debe cambiar radicalmente: probar un software no es una actividad que se limite al equipo designado para ello, es una tarea de todo programador. Cada persona será la encargada de probar aquellas secciones de código conformadas por ella. Cada sección de código que pueda fallar, debe ser probada.

Sería bueno definir el término probar. No se necesita ser un erudito para concluir que probar un software no es más que ejercitar cada una de las características funcionales del mismo, o técnicamente hablando, cada uno de sus requerimientos para detectar posibles anomalías o fallos – a los cuales comúnmente les damos el nombre de bugs –. Ahora, lo importante cuando hablamos de TDD, es definir precisamente cuáles serán los medios para hacer estas pruebas.

Un rápido estudio bibliográfico resalta dos tipos fundamentales de pruebas: pruebas unitarias o de unidad y pruebas de aceptación. Las primeras tienen su origen en la era anterior a la programación orientada a objetos y nos dicen que las pruebas individuales se concentrarán en unidades sencillas del sistema en vez de en el sistema completo. Las pruebas unitarias serán construidas de forma automatizada y ejecutadas regularmente durante el desarrollo del proyecto.

Las pruebas de aceptación sirven tanto al cliente como al equipo responsable del proyecto como una medida para determinar el progreso del producto en construcción. Estas pruebas serán creadas por los clientes y especificarán la funcionalidad del sistema desde la perspectiva del usuario. Normalmente, estas pruebas serán realizadas a mano y su ejecución estará estrechamente ligada con la conclusión de las etapas de entrega de los diferentes prototipos del producto.

Vamos a concentrarnos fundamentalmente en las pruebas unitarias ya que son las que tienen un mayor sentido desde el punto de vista del desarrollador. Para ello, vamos a recurrir a un ejemplo de código sencillo: la implementación más antigua de una función que determine si un número es primo o no. El código en Java es como sigue:
package com.samples;
 
public class Sample {
    public static boolean isPrime(int number) {
        for (int i = 2; i < number / 2 + 1; i++) {
            if (number % i == 0) {
                return false;
            }
        }
        return true;
    }
}
El ejemplo muestra una clase compuesta por una función estática la cual pretende devolver un valor booleano en dependencia de si el parámetro especificado es un número primo o no. Ahora bien, supongamos que queremos asegurarnos de que la función presente el comportamiento adecuado. Para esto, construimos un pequeño programa con el siguiente código:
package com.samples;
 
public class Main {
    public static void main(String[] args) {
 
        if (!Sample.isPrime(1) || !Sample.isPrime(2)) {
            System.out.println("ERROR");
        }
        else if (Sample.isPrime(4) || Sample.isPrime(6)) {
            System.out.println("ERROR");
        }
        else {
            System.out.println("OK");
        }
    }
}
El código anterior sencillamente ejercitará nuestra función pasando diferentes valores para comprobar que los resultados sean los esperados. En caso de existir alguna anomalía, se imprimirá el mensaje ERROR, mientras que si todo sucede tal y como se espera, la cadena OK aparecerá en pantalla. En pocos minutos hemos logrado construir un código y su correspondiente prueba. Si luego de cualquier cambio que sufra nuestro código ejecutamos su prueba correspondiente, podremos estar seguros si continúa trabajando o se introdujo algún error.

Cuando se desea hacer una prueba para una sección de código determinado, el escenario anterior será suficiente. El problema surge cuando deseamos que cada pieza de código que pueda fallar tenga su correspondiente prueba unitaria – precisamente estamos hablando de TDD –. Entonces necesitaremos mecanismos mucho más organizados y eficientes para clasificar y desarrollar cada una de las pruebas.

Es aquí donde entra en escena el proyecto xUnit. xUnit no es más que una plataforma para la realización de pruebas unitarias en cualquier lenguaje de programación. Existen versiones para casi todos los lenguajes como Java, Smalltalk, Delphi, C#, ASP, PHP, Visual Basic, C++ y Perl por solo mencionar algunos. Cada una de estas versiones cuenta con su propio nombre y diferentes características, pero todos persiguen el mismo objetivo final.

Veamos una prueba del ejemplo anterior haciendo uso de la biblioteca JUnit, diseñada para la construcción y ejecución de pruebas unitarias en lenguaje Java:
package com.samples;
 
import junit.framework.TestCase;
 
public class TestSample
    extends TestCase {
      
    public void testIsPrime() {
        assertTrue(Sample.isPrime(1));
        assertTrue(Sample.isPrime(2));
        assertFalse(Sample.isPrime(4));
        assertFalse(Sample.isPrime(6));
    }
}
Ahora el código resultante es mucho más claro, flexible y fácil de mantener. JUnit – como cualquier buen miembro del proyecto xUnit – nos brinda herramientas muy poderosas para la realización de pruebas unitarias. Si comparamos este código con el ejemplo primeramente visto, caeremos en la cuenta de lo sencillo y claro que puede resultar escribir una prueba unitaria.

Segundo Acto

En un mundo lleno de símbolos, xUnit no podía quedar atrás. La utilización de colores opuestos para representar lo bueno y lo malo ha sido una técnica que ha primado a lo largo de la historia. El verde y el rojo – colores del semáforo con significados obvios – son las luces que xUnit utiliza para su propia interpretación de lo correcto y lo incorrecto. Al ser ejecutadas las pruebas, una barra indicadora tomará el color correspondiente en dependencia de si nuestras pruebas detectan errores o no.

El ejemplo anterior dará como resultado una barra verde, ya que la función producirá correctamente el valor resultante. Veamos ahora otro ejemplo donde la prueba detectará rápidamente un error en el código. Para la realización de este ejemplo, utilizaré el lenguaje C#, tan popular en el mundo entero.

Supongamos que necesitamos crear una función que dados dos valores enteros devuelva la suma de ambos. Un ejemplo bien sencillo, cuya prueba haciendo uso de NUnit – versión xUnit para C# – quedará de la siguiente forma:
using NUnit.Framework;
 
namespace Samples.Test
{
    [TestFixture]
    public class TestCalculator
    {
        [Test]
        public void TestSum()
        {
            Assert.AreEqual(3, Calculator.Sum(1, 2));
        }
    }
}
Teniendo la prueba, podemos ejecutarla y observar – como es lógico – que la barra resultante será roja debido a que ni siquiera hemos implementado el código de la función a probar. Pasemos a implementarla y así lograr que la prueba brinde un resultado satisfactorio al ser ejecutada:
namespace Samples;
 
public class Calculator {
 
    public static int Sum(int number1, int number2)
    {
        return number1 + number1; // Error!
    }
}
Al ejecutar la prueba esta vez, la barra continuará indicando un error en nuestro código. Como ya deben haber notado, deliberadamente he introducido un error en la función, realizando la suma con el primer número solamente, por lo cual el resultado final siempre estará alterado. Si cambiamos el resultado de la función y conformamos adecuadamente la suma, la prueba correrá satisfactoriamente (barra verde). Errores como este en un código bastante complejo pueden tardar varios minutos en ser detectados sin la ayuda de pruebas unitarias.

Algo que me gustaría hacer notar en este punto es la forma de desarrollar el segundo ejemplo. Hemos construido la prueba antes de construir el código que logre que esta pueda ser ejecutada. Este estilo de programación es denominado Test-First Development – algo así como Desarrollo Primero de las Pruebas – y es el más citado por los programadores de TDD.

Tercer Acto

Dado que mi objetivo no es caer en detalles particulares de la plataforma xUnit, y mucho menos en sus variantes JUnit y NUnit, hablemos un poquito más de un conjunto de elementos teóricos imprescindibles, o resumiendo rápidamente en una pregunta ¿por qué debo construir pruebas unitarias?

Mi propio punto de vista – al menos el mismo que logró convencerme cuando aún era bastante escéptico respecto a estas prácticas – pudiera exponerlo en los siguientes cinco puntos:
  1. Permiten detectar errores rápidamente en tiempo de diseño. Contar con un paquete de pruebas permitirá detectar de forma clara y rápida los defectos del código a medida que vayan surgiendo. Esto permitirá reducir considerablemente las horas necesarias de depuración que tanto molestan. El resultado será un producto mucho más robusto y libre de errores.

  2. Brindan la confianza necesaria para realizar modificaciones en el código. ¿Alguna vez ha escuchado la célebre frase "Si funciona no lo toque"?. Apuesto que sí. Ciertamente nada es más cierto, pero yo preferiría – con el perdón del autor – modificar ligeramente la frase a la hora de aplicarla al contexto del que estamos tratando: "Si funciona, y no tiene forma de probar después de los cambios, no lo toque". Un conjunto de pruebas unitarias nos dará el coraje necesario para enfrentar cualquier reestructuración – o simples cambios – en nuestro código fuente. Bastará con ejecutar las pruebas para determinar si la operación resultó o si debemos dar marcha atrás.

  3. Son la fuente de documentación más efectiva de nuestro código. Recuerdo que corría rápidamente hasta el ejemplo en el libro de texto cada vez que enfrentaba un problema cuya solución se me antojaba esquiva. Los ejemplos siempre han sido una herramienta muy poderosa para ganar conocimientos. Precisamente una prueba unitaria no es más que una forma de utilizar nuestro código, un ejemplo ejecutable de cómo explotarlo. Toneladas de documentación escrita pudieran opacarse – y hasta borrarse completamente – con un solo ejemplo.

  4. Escribirlas resulta una tarea bastante divertida. Varios autores coinciden que escribir pruebas unitarias puede resultar una tarea adictiva. Mi propia experiencia es que están en lo cierto. El paso del rojo al verde resulta una experiencia única que bien vale la pena disfrutar.

  5. Cualquier característica de un programa que no tenga una prueba automatizada, simplemente no existe. Este punto, a pesar de ser una cita textual, no podía dejar de incluirlo en el listado. Si algo no se ha probado ¿cómo podemos asegurar que realmente funciona? O mejor aún ¿podemos asegurar que siempre funcionará?
Estas son las fuerzas que han impulsado a tantos a convertir esta práctica como propia. Cada vez se publican más libros y artículos referentes al tema, se realizan conferencias, se imparten seminarios y se emplean los más diversos métodos de educación para llevar el desarrollo de pruebas a cada rincón del mundo del desarrollo de software.

Por último, y unido a todo esto, los entornos modernos de desarrollo visual de lenguajes de programación incluyen herramientas para la realización de pruebas unitarias, o al menos vías simples para integrarlos con las versiones existentes de xUnit. Tales son los casos de Eclipse, JBuilder, Visual Studio .NET, VisualAge, Emacs, etc. Los dos primeros para Java, el tercero para C# / VB.NET, el cuarto para Smalltalk y el último para C++.

Baja el Telón

La programación de pruebas unitarias es una tarea que resultará siempre chocante a cualquier programador en el mundo entero. Increíblemente, los ateos se convierten en fervientes creyentes una vez que experimentan a cabalidad todas sus ventajas. Su realización – mucho más si hablamos de TDD – es una tarea, como ya decía, que requiere mucha constancia y disciplina.

Son muchas las técnicas y trucos que se han escrito para escribir pruebas unitarias efectivas. Son unos cuantos los libros que engrosan los estantes en bibliotecas y librerías hablando de estos temas. No se concibe un programador moderno que no incluya la palabra prueba en su vocabulario.

Es nuestra oportunidad ahora para experimentar algo así. Empezar por lo simple hasta que logremos cada vez más adentrarnos en las más diversas formas de la construcción de pruebas. La práctica sistemática nos convertirá en sus más fieles seguidores. Déles una oportunidad y verá que no se arrepentirá.

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.