Pequeña introducción al Test Driven Development en C++

Normalmente, cuando uno desarrolla un programa, lo que hace para ver si funciona es probar el programa con unos datos de entrada y ver si la salida es correcta.Existe una técnica de programación que consiste en escribir pruebas automáticas para programas que aún no han sido escritos, de forma que al principio las pruebas evidentemente fallan (y si no fallan, es que no valen para nada) y, cuando el programa esté completo, funcionan.Esto puede parecer una carga de trabajo excesiva pero, siempre que se use con sentido común, tiene varias ventajas:

  • Se define el prototipo de entrada y salida de datos desde el principio.
  • Se descubren errores prematuros que podrían pasar desapercibidos y hacernos dar mil vueltas por el código tratando de averiguar dónde está el problema.
  • Constituyen una prueba de que tu programa funciona, diga lo que diga el profesor.
  • Como son automáticos (los hace el ordenador, no tú), puedes ejecutarlos siempre que escribas algún trozo de código nuevo, o cambies algo, para asegurarte que no has roto nada. Esto aumenta la confianza entre miembros de equipos de desarrollo.

Querría matizar que hay que usarlos con sentido común. Si no somos capaces de descubrir si el problema está en nuestra función de prueba automática o en el código de la aplicación que hemos escrito, no valen para nada. No obstante, coger práctica en la escritura de pruebas conlleva un tiempo de aprendizaje.

Un ejemplo sencillo

Para que todo este rollo deje de sonar a chino, vayamos con un ejemplo facilito:

char pasar_caracter_a_mayusculas(char caracter) {
        int const SEPARACION_MINUS_MAYUS = 'A'-'a';
        caracter += SEPARACION_MINUS_MAYUS;
        return caracter;
        }

string pasar_texto_a_mayusculas(string texto) {
        // leer cada caracter y pasarlo a mayúsculas
        for (int i=0; i<texto.length(); i++) {
                texto[i] = pasar_caracter_a_mayusculas(texto[i]);
                }
        return texto;
        }

¿Ves el error? Porque hay uno… Está claro que es difícil cazarlo a simple vista, pero si probamos la función con un texto al azar nos daremos cuenta enseguida del problema:

int main(int argc, char* argv[]) {
        string text = "Los programadores.";
        cout << pasar_texto_a_mayusculas(text);
        return 1;
        }

Lo ejecutamos y aparece:

,OSPROGRAMADORES

Vale, tenemos un error en el código: se intentan pasar a mayúsculas tanto los caracteres que no son letras como los que ya son mayúsculas. En vez de empezar a retocar el código directamente, vamos a escribir una función que demuestra que nuestra función “pasar_texto_a_mayusculas” tiene un fallo:

bool prueba_pasar_texto_a_mayusculas(string texto_entrada, string texto_salida) {
        if (pasar_texto_a_mayusculas(texto_entrada) == texto_salida)
                return true;  // Test OK!
        else
                return false;  // Test Failed!
        }

int main(int argc, char* argv[]) {
        if (prueba_pasar_texto_a_mayusculas("Los programadores.", "LOS PROGRAMADORES."))
                cout << "pasar_texto_a_mayusculas funciona!n";
        else
                cout << "pasar_texto_a_mayusculas falla!n";
        return 1;
        }

Lo ejecutamos y aparece por la pantalla:

pasar_texto_a_mayusculas falla!

Ahora que hemos demostrado que hay un problema, vamos a escribir el código para arreglarlo. Cuando lo arreglemos, la prueba lo demostrará:

char pasar_caracter_a_mayusculas(char caracter) {
    int const SEPARACION_MINUS_MAYUS = 'A'-'a';
    if (caracter >= 'a' && caracter <= 'z')  // caracter es una letra minúscula
        caracter += SEPARACION_MINUS_MAYUS;
    return caracter;
    }

Si ejecutamos de nuevo nuestro programa veremos:

pasar_texto_a_mayusculas funciona!

¡Genial!

Observaciones

En este ejemplo, el error no estaba en la función que hemos comprobado, sino en otra a la que la primera llamaba. Idealmente, se debería escribir una función de prueba por cada función “real” que se implementa, para que sepamos inmediatamente en qué función está el fallo.

No basta con probar la función con un caso general: es muy importante probarla con casos límite, como textos vacíos, todo mayúsculas, todo minúsculas, etc. La mayoría de las veces, los problemas aparecen en esos casos límite.

De nuevo, hay que usar el sentido común: si no te sientes cómodo escribiendo funciones de prueba y tardas horas en hacerlo, olvídalo. Es sólo una técnica que puede ser tanto una ayuda maravillosa como una lacra absurda. Lo importante es saber que existe, por si algún día nos interesase usarla.

Las funciones de prueba son todas muy parecidas: consisten en pasar una serie de argumentos de entrada a una función y comprobar que la salida es la esperada. Usa el clásico copiar & pegar siempre que puedas. También existen frameworks (programas que sirven para escribir programas basados en ellos) que facilitan la tarea, aunque yo no he usado ninguno de C++ todavía.

Agradecimientos

Gracias a la comunidad de Python y Zope/Plone por hacer que me llegue a gustar tanto el Test-Driven Development como para que me anime a escribir sobre ello, y a la gente que ha confiado en mí para que pueda publicar esto en el foro de MareMonstrum. :-)

Leave a Reply

Your email address will not be published. Required fields are marked *