Bienvenidos sean a este post, hoy veremos un ejemplo de TDD.
En el post anterior mencionamos como son los tres pasos de TDD. Aqui veremos como implementarlo mediante un ejemplo, este calculara una algebra matricial 2D. Esta es una planilla de clase que puede contener una matriz de m por n para todos los tipos. La algebra matricial incluye adicion, substraccion, multiplicacion y division de matrices, y tambien tiene habilidades de operacion de elementos.
Paso 1 – Escribir un test falllido
Para comenzar con este paso, necesitaremos lo siguiente:
- Crear un objeto Mat desde un numero de filas y columnas, su valor predeterminado debe ser 0 x 0 lo cual representa una matriz vacia
- Imprimir sus elementos fila por fila
- Obtener el tamaño de la matriz de las filas y columnas
Con esto ya tenemos la base del diseño para nuestro test, para este usaremos a boost. Por lo tanto, es necesario que instalen todas las librerias para poder utilizarlo. Lo cual hicimos en este post, pero en caso de necesitar instalarlo usen el siguiente comando:
$ sudo apt-get install libboost-all-dev
Con esto instalado, nuestro primer paso sera crear un archivo con el nombre de boost_tdd.cpp y agregaremos el siguiente codigo:
boost_tdd.cpp
#define BOOST_TEST_MODULE test_tdd
#include <boost/test/included/unit_test.hpp>
#include "mat_2d_tdd.h"
BOOST_AUTO_TEST_SUITE(suite_tdd)
BOOST_AUTO_TEST_CASE(test_caso_1) {
Mat<int> x(2, 3);
x.print("int x=");
BOOST_TEST(2 == x.rows());
BOOST_TEST(3 == x.cols());
Mat<float> y;
y.print("float y=");
BOOST_TEST(0 == y.rows());
BOOST_TEST(0 == y.cols());
Mat<char> z(1,10);
z.print("char z=");
BOOST_TEST(1 == z.rows());
BOOST_TEST(10 == z.cols());
}
BOOST_AUTO_TEST_SUITE_END()
Lo primero que haremos es establecer el nombre del test. Importamos la libreria de boost para efectuar un test de unidad y tambien importamos a la libreria que tendra la clase para calcular la matriz en 2D, la cual aun no existe, para despues crear el conjunto de test y en este agregaremos un test donde haremos tres implementaciones de la clase que se encargara de nuestra matriz, y usaremos a BOOST_TEST para chequear algunas condiciones. Si lo prueban ahora devolvera un error porque nuestro programa no existe aun. Por esta razon pasaremos al segundo paso.
Paso 2 – Desarrollar el codigo
Este codigo tendra como meta principal pasar el test anterior y cumpllir algunos de los requerimientos del diseño. Lo siguiente es crear el archivo de encabezado que incluimos en el paso anterior. Para ello debemos crear un archivo con el nombre de mat_2d_tdd.h y agregaremos el siguiente codigo:
mat_2d_tdd.h
#ifndef __mat_2d_TDD__
#define __mat_2d_TDD__
#include <iostream>
#include <assert.h>
template< class T>
class Mat {
public:
Mat(const uint32_t m=0, const uint32_t n=0);
Mat(const Mat<T> &rhs) = delete;
~Mat();
Mat<T>& operator = (const Mat<T> &x) = delete;
uint32_t rows() { return m_rows; }
uint32_t cols() { return m_cols; }
void print(const char* str) const;
private:
void creatBuf();
void deleteBuf();
uint32_t m_rows;
uint32_t m_cols;
T* m_buf;
};
#include "mat_2d_tdd.cpp"
#endif
Como comentamos esta sera la clase encargada de crear la matriz. La primera particularidad es la macro condicional donde verifica si no existe esta, en caso de ser verdadero procede a crearla y define la clase. A partir de ahora, cada vez que verifique se omitira esto y esto hara que no se cargue nuevamente en memoria. Basicamente establecera las filas y columnas, asi como tambien tenemos metodos para obtener los valores de ambos, asi como tambien el constructor con valores predeterminados y la eliminacion del constructor de copia y de la sobrecarga de asignacion. Tambien tenemos los prototipos de tres metodos pero estos lo definiremos en un momento. Con nuestro nuestro archivo de encabezado establecido, nos resta definir los prototipos de este. Para esto, deben crear un archivo con el nombre de mat_2d_tdd.cpp y le agregaremos el siguiente codigo:
mat_2d_tdd.cpp
#include "mat_2d_tdd.h"
using namespace std;
template< class T>
Mat<T>::Mat(const uint32_t m, const uint32_t n) : m_rows(m),
m_cols(n),
m_buf(NULL)
{
creatBuf();
}
template< class T>
Mat<T> :: ~Mat()
{
deleteBuf();
}
template< class T>
void Mat<T>::creatBuf()
{
uint32_t sz = m_rows * m_cols;
if (sz > 0) {
if (m_buf) { deleteBuf();}
m_buf = new T[sz];
assert(m_buf);
}
else
{
m_buf = NULL;
}
}
template< class T>
void Mat<T>::deleteBuf()
{
if (m_buf) {
delete[] m_buf;
m_buf = NULL;
}
}
template< class T>
void Mat<T> ::print(const char* str) const
{
cout << str << endl;
cout << m_rows << " x " << m_cols << "[" << endl;
const T *p = m_buf;
for (uint32_t i = 0; i<m_rows; i++) {
for (uint32_t j = 0; j < m_cols; j++) {
cout << *p++ << ", ";
}
cout << "\n";
}
cout << "]\n";
}
Primero importamos al archivo anterior y luego unicamnte definiremos a cada uno de los prototipos del archivo de encabezado. Definimos al constructor para que inicie a las propiedades y llama al metodo para crear un buffer. El destructor llama al metodo que elimina dicho buffer. El metodo de creacion de buffer establece uno si el tamaño (sz) es mayor a cero de lo contrario lo deja como nulo. El metodo de eliminacion se encarga de eliminarlo en memoria y establecerlo como nulo. El ultimo metodo se encarga los valores almacenados en el buffer. Todo esto al hacer generico nos permite usar a cualquier tipo, tal como hacemos en el test del paso anterior. Ahora si vamos a compilarlo de la siguiente manera:
$ g++ -g boost_tdd.cpp -o boost_tdd
Observen que compilamos al codigo del test, sin necesidad de pasar a la otra libreria o archivo de definicion. Si lo ejecutan deben obtener una salida similar a esta:
$ ./boost_tdd
Running 1 test case...
int x=
2 x 3[
-1236929890, 21888, 0,
0, -300356864, 21893,
]
float y=
0 x 0[
]
char z=
1 x 10[
▒, ▒, E, ▒, ▒, U, , , , ,
]
*** No errors detected
$
Al tener la libreria para poder crear los objetos y poder trabajar en los test, y este al ser generica funcionara perfectamente y tendremos ese mensaje al final. Hasta aca parece perfecto pero ahora debemos pasar al siguiente paso.
Paso 3 – Refactorear
Hasta aca sabemos que nuestra aplicacion funciona pero el cliente quiere que agreguemos algunas nuevas interfaces para realizar nuevas tareas. Veamos algunas sugerencias:
- Crear un matriz m x n con un valor inicial para todos los elementos
- Agregar un metodo para obtener el total de elementos de la matriz
- Agregar un metodo para determinar cuando la matriz este vacia
Con nuestras nuevas propuestas agregadas, pasemos a modificar el archivo de testing de la siguiente manera:
#define BOOST_TEST_MODULE test_tdd
#include <boost/test/included/unit_test.hpp>
#include "mat_2d_tdd.h"
BOOST_AUTO_TEST_SUITE(suite_tdd)
BOOST_AUTO_TEST_CASE(test_caso_1) {
Mat<int> x(2, 3);
x.print("int x=");
BOOST_TEST(2 == x.rows());
BOOST_TEST(3 == x.cols());
Mat<float> y;
y.print("float y=");
BOOST_TEST(0 == y.rows());
BOOST_TEST(0 == y.cols());
Mat<char> z(1,10);
z.print("char z=");
BOOST_TEST(1 == z.rows());
BOOST_TEST(10 == z.cols());
}
BOOST_AUTO_TEST_CASE(test_caso_2)
{
Mat<int> x(2, 3, 10);
x.print("int x=");
BOOST_TEST( 6 == x.numel() );
BOOST_TEST( false == x.empty() );
Mat<float> y;
BOOST_TEST( 0 == y.numel() );
BOOST_TEST( x.empty() );
}
BOOST_AUTO_TEST_SUITE_END()
En este caso, agregamos un nuevo tipo de test para que evalue no solo la nueva forma de trabajar con los valores, asi como tambien con el metodo para saber cuantos elementos tenemos (numel) y otro para saber si esta vacio o no (empty). Lo siguiente es pasar al archivo de encabezado, mat_2d_tdd.h, y modificaremos a la parte publica de la clase de la siguiente manera:
public:
Mat(const uint32_t m=0, const uint32_t n=0);
Mat(const uint32_t m, const uint32_t n, const T initVal);
Mat(const Mat<T> &rhs) = delete;
~Mat();
Mat<T>& operator = (const Mat<T> &x) = delete;
uint32_t rows() { return m_rows; }
uint32_t cols() { return m_cols; }
uint32_t numel() { return m_rows * m_cols;}
uint32_t empty() { return 0==m_rows || 0==m_cols; }
void print(const char* str) const;
Aqui agregaremos el prototipo del nuevo constructor para que reciba tres valores iniciales. Y luego agregamos los metodos para determinar la cantidad de elementos y si la matriz esta vacia. Ahora pasemos ahora al archivo mat_2d_tdd.cpp y agreguemos la definicion del nuevo constructor:
template< class T>
Mat<T>::Mat(const uint32_t m, const uint32_t n, const T initVal) : m_rows(m),
m_cols(n),
m_buf(NULL)
{
creatBuf();
for (int i = 0; i < m_rows*m_cols; ++i) {
m_buf[i] = initVal;
}
}
Esto no solo recibe los dos valores anteriores, sino que con el nuevo valor en el tercer argumento iniciaremos a todos los elementos de la matriz. Con esto pueden compilarlo nuevamente y ver como funciona ahora con todas estas modificaciones. Veamos como lo hace:
$ ./boost_tdd
Running 2 test cases...
int x=
2 x 3[
1809193121, 21988, 0,
0, 901979136, 21985,
]
float y=
0 x 0[
]
char z=
1 x 10[
▒, ▒, ▒, k, ▒, U, , , , ,
]
int x=
2 x 3[
10, 10, 10,
10, 10, 10,
]
boost_tdd.cpp(33): error: in "suite_tdd/test_caso_2": check x.empty() has failed ['0' evaluates to false]
*** 1 failure is detected in the test module "test_tdd"
$
Veamos el primer test, ahora nos devolvio una serie de numeros aleatorios porque al no ser iniciado con la nueva modificacion del tercer valor sigue trabajando como antes. El resto siguen iguales pero observemos el segundo test. Aca si nos inicio todos los valores con el nuevo valor enviado y no tenemos ninguna notificacion para el caso de numel y empty del primer test pero si tenemos una falla para la segunda condicion de este test. Al momento de verificar si esta vacio pasamos el valor de 0 y este no es asi porque nuestras matrices al estar iniciadas no cumpliran esa condicion. Por lo tanto, se deben hacer algunos ajustes y seguramente surjan nuevas caracteristicas que el cliente quiera agregar hasta que lleguemos a un punto donde estemos todos de acuerdo.
En resumen, hoy hemos visto un ejemplo de TDD, como se piensa el diseño base, asi como en base a este creamos nuestro primer paso que es el test, luego creamos el codigo para satisfacer el test del paso anterior, y como este nos sirve para agregar mas elementos al test para satisfacer nuevas caracteristicas solicitadas mediante nuevo codigo que satisfaga al nuevo test. Espero les haya resultado de utilidad sigueme en tumblr, Twitter o Facebook para recibir una notificacion cada vez que subo un nuevo post en este blog, nos vemos en el proximo post.


Donación
Es para mantenimento del sitio, gracias!
$1.50
