3.2. Un framework de pruebas unitarias sencillo

Escribir software es todo sobre encontrar requerimientos.[20] Crear estos requerimientos es difícil, y pueden cambiar de un día a otro; podría descubrir en una reunión de proyecto semanal que lo que ha empleado la semana haciendo no es exactamente lo que los usuarios realmente quieren.

Las personas no pueden articular requerimientos de software sin muestrear un sistema de trabajo en evolución. Es mucho mejor especificar un poco, diseñar un poco, codificar un poco y probar un poco. Entonces, después de evaluar el resultado, hacerlo todo de nuevo. La habilidad para desarrollar con una moda iterativa es uno de los mejores avances del enfoque orientado a objetos, pero requiere programadores ágiles que pueden hacer código fuerte. El cambio es duro.

Otro ímpetu para el cambio viene de usted, el programador. El artífice que hay en usted quiere continuamente mejorar el diseño de su código. ¿Qué programador de mantenimiento no ha maldecido el envejecimiento, el producto de la compañía insignia como un mosaico de espaguetis inmodificable, enrevesado? La reluctancia de los supervisores en permitir que uno interfiera con un sistema que funciona le roba al código la flexibilidad que necesita para que perdure. Si no está roto, no arreglarlo finalmente le da el camino para, no podemos arreglarlo reescribámoslo. El cambio es necesario.

Afortunadamente, nuestra industria está creciendo acostumbrada a la disciplina de refactoring, el arte de reestructura internamente código para mejorar su diseño, sin cambiar su comportamiento.[21] Tales mejoras incluyen extraer una nueva función de otra, o de forma inversa, combinar funciones, reemplazar una función con un objeto; parametrizar una función o clase; y reemplazar condicionales con polimorfismo. Refactorizar ayuda al código evolucionar.

Si la fuerza para el cambio viene de los usuarios o programadores, los cambios hoy pueden destrozar lo trabajado ayer. Necesitamos un modo para construir código que resista el cambio y mejoras a lo largo del tiempo.

La Programación Extrema (XP)[22] es sólo uno de las muchas prácticas que motivan la agilidad. En esta sección exploramos lo que pensamos es la clave para hacer un desarrollo flexible, incremental que tenga éxito: un framework de pruebas unitarias automatizada fácil de usar. (Note que los probadores, profesionales de software que prueban el código de otros para ganarse la vida, son todavía indispensables. Aquí, estamos simplemente describiendo un modo para ayudar a los desarrolladores a escribir mejor código.)

Los desarrolladores escriben pruebas unitarias para conseguir confianza para decir las dos cosas más importantes que cualquier desarrollador puede decir:

1. Entiendo los requerimientos.

Mi código cumple esos requerimientos (hasta donde yo sé)

No hay mejor modo para asegurar que sabe lo que el código que está por escribir debería hacer mejor que escribir primero pruebas unitarias. Este ejercicio sencillo ayuda a centrar la mente en las tareas siguientes y probablemente guiará a código que funcionalmente más rápido mejor que sólo saltar a codificar. O, expresarlo en términos XP:

Probar + programar es más rápido que sólo programar.

Escribir primero pruebas sólo le protegen contra condiciones límite que podrían destrozar su código, por lo tanto su código es más robusto.

Cuando su código pasa todas sus pruebas, sabe que si el sistema no está funcionando, su código no es probablemente el problema. La frase todas mis pruebas funcionan es un fuerte razonamiento.

3.2.1. Pruebas automatizadas

Por lo tanto, ¿qué aspecto tiene una prueba unitaria? Demasiado a menudo los desarrolladores simplemente usan alguna entrada correcta para producir alguna salida esperada, que examinan visualmente. Existen dos peligros en este enfoque. Primero, los programas no siempre reciben sólo entradas correctas. Todos sabemos que deberíamos probar los límites de entrada de un programa, pero es duro pensar esto cuando está intentando simplemente hacer que las cosas funcionar. Si escribe primero la prueba para una función antes de comenzar a codificar, puede ponerse su traje de probador y preguntarse a si mismo, ¿qué haría posiblemente destrozar esto? Codificar una prueba que probará la función que escribirá no es erróneo, y luego ponerte el traje de desarrollador y hacerlo pasar. Escribirá mejor código que si no había escrito la prueba primero.

El segundo peligro es que esperar una salida visualmente es tedioso y propenso a error. La mayoría de cualquier tipo de cosas que un humano puede hacer un ordenador puede hacerlas, pero sin el error humano. Es mejor formular pruebas como colecciones de expresiones boolean y tener un programa de prueba que informa de cualquier fallo.

Por ejemplo, suponga que necesita construir una clase Fecha que tiene las siguientes propiedades:

Una fecha puede estar inicializada con una cadena (AAAAMMDD), 3 enteros (A, M, D), o nada (dando la fecha de hoy).

Un objecto fecha puede producir su año, mes y día o una cadena de la forma AAAAMMDD.

Todas las comparaciones relacionales están disponibles, además de calcular la duración entre dos fechas (en años, meses, y días).

Las fechas para ser comparadas necesitan poder extenderse un número arbitrario de siglos(por ejemplo, 16002200).

Su clase puede almacenar tres enteros que representan el año, mes y día. (Sólo asegúrese que el año es al menos de 16 bits de tamaño para satisfacer el último punto.) La interfaz de su clase Fecha se podría parecer a esto:

//: C02:Date1.h
// A first pass at Date.h.
#ifndef DATE1_H
#define DATE1_H
#include <string>

class Date {
public:
  // A struct to hold elapsed time:
  struct Duration {
    int years;
    int months;
    int days;
    Duration(int y, int m, int d)
    : years(y), months(m), days(d) {}
  };
  Date();
  Date(int year, int month, int day);
  Date(const std::string&);
  int getYear() const;
  int getMonth() const;
  int getDay() const;
  std::string toString() const;
   friend bool operator<(const Date&, const Date&);
   friend bool operator>(const Date&, const Date&);
   friend bool operator<=(const Date&, const Date&);
   friend bool operator>=(const Date&, const Date&);
   friend bool operator==(const Date&, const Date&);
   friend bool operator!=(const Date&, const Date&);
  friend Duration duration(const Date&, const Date&);
};
#endif // DATE1_H ///:~

Listado 3.2. C02/Date1.h


Antes de que implemente esta clase, puede solidificar sus conocimientos de los requerimientos escribiendo el principio de un programa de prueba. Podría idear algo como lo siguiente:

//: C02:SimpleDateTest.cpp
//{L} Date
#include <iostream>
#include "Date.h" // From Appendix B
using namespace std;

// Test machinery
int nPass = 0, nFail = 0;
void test(bool t) { if(t) nPass++; else nFail++; }

int main() {
  Date mybday(1951, 10, 1);
  test(mybday.getYear() == 1951);
  test(mybday.getMonth() == 10);
  test(mybday.getDay() == 1);
  cout << "Passed: " << nPass << ", Failed: "
       << nFail << endl;
}
/* Expected output:
Passed: 3, Failed: 0
*/ ///:~

Listado 3.3. C02/SimpleDateTest.cpp


En este caso trivial, la función test( ) mantiene las variables globales nAprobar y nSuspender. La única revisión visual que hace es leer el resultado final. Si una prueba falla, un test( ) más sofisticado muestra un mensaje apropiado. El framework descrito más tarde en este capítulo tiene un función de prueba, entre otras cosas.

Puede ahora implementar la clase Fecha para hacer pasar estas pruebas, y luego puede proceder iterativamente hasta que se satisfagan todos los requerimientos. Escribiendo primero pruebas, es más probable que piense en casos límite que podrían destrozar su próxima implementación, y es más probable que escriba el código correctamente la primera vez. Como ejercicio podría realizar la siguiente versión de una prueba para la clase Fecha:

//: C02:SimpleDateTest2.cpp
//{L} Date
#include <iostream>
#include "Date.h"
using namespace std;

// Test machinery
int nPass = 0, nFail = 0;
void test(bool t) { if(t) ++nPass; else ++nFail; }

int main() {
  Date mybday(1951, 10, 1);
  Date today;
   Date myevebday("19510930");

  // Test the operators
  test(mybday < today);
  test(mybday <= today);
  test(mybday != today);
  test(mybday == mybday);
  test(mybday >= mybday);
  test(mybday <= mybday);
  test(myevebday < mybday);
  test(mybday > myevebday);
  test(mybday >= myevebday);
  test(mybday != myevebday);

  // Test the functions
  test(mybday.getYear() == 1951);
  test(mybday.getMonth() == 10);
  test(mybday.getDay() == 1);
  test(myevebday.getYear() == 1951);
  test(myevebday.getMonth() == 9);
  test(myevebday.getDay() == 30);
  test(mybday.toString() == "19511001");
  test(myevebday.toString() == "19510930");

  // Test duration
  Date d2(2003, 7, 4);
  Date::Duration dur = duration(mybday, d2);
  test(dur.years == 51);
  test(dur.months == 9);
  test(dur.days == 3);

  // Report results:
  cout << "Passed: " << nPass << ", Failed: "
       << nFail << endl;
} ///:~

Listado 3.4. C02/SimpleDateTest2.cpp


Esta prueba puede ser desarrollada por completo. Por ejemplo, no hemos probado que duraciones grandes son manejadas correctamente. Pararemos aquí, pero coja la idea. La implementación entera para la case Fecha está disponible en los ficheros Date.h y Date.cpp en el apéndice.[23]