jasmine: 2. TDD y BDD

Además de seguir con jasmine, veremos a vuelapluma en qué consiste el TDD, el diseño orientado a test.

Lawrence Alma-Tadema

archivado en: JavaScript / 19 febrero, 2016 / taller:

Como vimos, los test son un conjunto de algoritmos que comprueban si determinadas partes del código se ajustan a las especificaciones indicadas. Por ejemplo, un test puede servir para comprobar que el resultado de sumar dos y dos es cuatro. Sin entrar en disquisiciones académicas, los test se pueden clasificar en tres grandes conjuntos:

  1. Test unitarios, que analizan partes atómicas del código.
  2.  Test end to end (E2E), que analizan flujos completos de una aplicación.
  3. Test de integración, pruebas totales que comprueban también la relación entre el front y el back y que normalmente se lanzan de forma regular (continua), por ejemplo, cuando se pusea contra master en un sistema de control de versiones del tipo git o Subversion.

Antes de seguir con Jasmine, vamos a profundizar un poco más en el primer tipo de test, los unitarios, y en el diseño orientado a test (TDD).

Casos de uso

Hay dos estrategias para preparar los test. Una es escribir el código primero y luego picar unos test que comprueben que los métodos más relevantes de la aplicación se comportan según lo esperado, que sirve para esos escenarios alocados, de plazos imposibles, donde hay que trabajar a toda pastilla porque un comercial se ha comprometido a una entrega desquiciada; y otra es diseñar primero los test, que es cuando adquieren mayor sentido. Pero para entender el alcance de esta última afirmación, antes vamos a recordar cómo funciona el mundo real del desarrollo de software.

Cuando se comienza un proyecto, raro es que se sepa con antelación todo lo que debe hacer una aplicación, sino solo unas ideas generales: debe gestionar las mercancías que entran y salen de unos almacenes, es un wizard para un proceso de compra de billetes de viaje, queremos una guía con los restaurantes de una ciudad... Y, a pesar de la documentación más o menos prolífica que se prepara antes de empezar, como los funcionales o las propuestas de diseño, a lo largo del desarrollo van surgiendo cambios naturales que pueden ser realmente drásticos. ¡Diantres, no nos dimos cuenta que a veces las mercancías pueden llegar deterioradas, hay que incluir esa posibilidad! Ale op, a cambiar interfaces, modelos, comprobaciones de formularios y un largo etcétera que suele llevar inexorablemente a mundo ñapismo y a tener que comprobar todos los procesos por si nos hemos cargado algo con el cambio, una labor tediosa que si no tenemos los test preparados hay que hacer a mano, con todo lo que conlleva esto. Pero si preparamos los test primero, entre otras cosas, conseguimos que:

  • Desde un principio, se obligue a todas las partes implicadas a reflexionar sobre lo que debe hacer realmente la aplicación.
  • Cuando surgen cambios durante el desarrollo estemos mejor preparados para afrontarlos.
  • Se rebaje la labor de control del trabajo realizado por los perfiles más junior, puesto que los más avezados -analistas y arquitectos- cuentan con herramientas que les ayudan a hacer su trabajo.
  • Se planifique mejor el código que se debe desarrollar, lo que contribuye a seguir una filosofía YAGNI (You Ain't Gonna Need It), que vendría a significar: escribe solo el código que necesitas realmente y ni una línea más, lo cual se traduce en tiempo y, por lo tanto, en dinero.

Esta es la idea que subyace tras el denominado BDD, behavior-driven development, que es una variante sutil del TDD:

  • Primero se describen unos «casos de uso», por ejemplo, el sistema debe contemplar el caso de que una mercancía llega deteriorada, se debe bloquear el proceso de pago si la tarjeta de crédito es incorrecta, etcétera.
  • Luego se preparan unos test que comprueben que se cumplen todas esas especificaciones.
  • Se escribe el código necesario para superar esos test.

Y todo esto en un proceso de refactorización constante en función del devenir del proyecto.

Aplicar este marco teórico al cien por cien a la vida real, en mi opinión, es una quimera por razones que explicaré otro día, pero sí puede ser un buen punto de fuga para afrontar determinados proyectos, los que van para largo y superan las miles de líneas código. Y como esta entrada está quedando infumable, un solo apunte teórico más y ya pasamos al código.

F.I.R.S.T

Como explica Carlos Blé Jurado en Diseño ágil con TDD, un ensayo muy ameno y preciso sobre el tema, los test unitarios deben ser

Atómicos: «un test probará un solo comportamiento de un método de una clase. El mismo método puede presentar distintas respuestas ante distintas entradas o distinto contexto. El test unitario se ocupará exclusivamente de uno de esos comportamientos, es decir, de un único camino de ejecución».

Independientes: «un test no puede depender de otros para producir un resultado satisfactorio. No puede ser parte de una secuencia de tests que se deba ejecutar en un determinado orden».

Inocuos: «un test no altera el estado del sistema. Al ejecutarlo una vez, produce exactamente el mismo resultado que al ejecutarlo veinte veces. No altera la base de datos, ni envía emails ni crea ficheros, ni los borra. Es como si no se hubiera ejecutado».

Rápidos: «un test tiene que ser porque ejecutamos un gran número de tests cada pocos minutos».

Estas características -que se corresponden con el acrónimo inglés F.I.R.S.T (Fast, Independent, Repeatable, Small y Transparent)- se entienden bien con un ejemplo. Imaginemos que tenemos que hacer una aplicación para llevar las cuentas de algo, por ejemplo, los gastos mensuales, pues en ese caso parece que cuanto menos tendríamos que preparar los siguientes test:

  • Si se añade un ingreso, al total debe incorporarse esa cantidad.
  • Si se añade un gasto, al total debe incorporarse esa cantidad.
  • Si lo que se va ingresar no es un número, debe saltar un error.
  • Si lo que se va gastar no es un número, debe saltar un error...

Es decir, debe haber un test para cada una de las operaciones, de los métodos, relevantes de la aplicación y cada uno debe funcionar de manera autónoma... o al menos lo más independiente posible. Y para que los test no alteren la base de datos, trabajaremos con mocks, con datos de palo que simularán las respuestas de los modelos.

Suiting 😛

Vamos a llevar a la práctica estas ideas y de paso recordamos algunas cosas de la entrada anterior de esta serie, donde empezamos a ver jasmine, que es un framework para lanzar pruebas unitarias en javaScript. Nos piden una aplicación que compruebe si un usuario está registrado para darle acceso a un contenido, una página de login para entendernos, y para eso hay que comprobar dos campos, uno con el nombre usuario -que es su correo- y otro con la contraseña.

Lo primero, como hemos visto, es definir unos casos de uso, en los cuales se indica el qué se debe hacer y no el cómo:

  • La aplicación debe permitir el acceso si el usuario está registrado.
  • Se debe prohibir el acceso si no lo está.
  • Hay que avisar al usuario que los datos son incorrectos cuando no está registrado.
  • Mientras se realiza la comprobación en back, hay que informar al usuario de que la operación está en curso.
  • ...
  • dfs

Con estas premisas, podemos empezar a escribir un test, una suite en jasmineano, y para que se entienda mejor iré poniendo el código js necesario para superarlo.

Lo primero que debemos hacer es comprobar si existe un objeto para trabajar el formulario, así que empezamos una spec, con un primer expectation, esto es, una función en la que esperamos que algo sea de determinada manera (por favor, repasa la primera entrada si esto te suena a chino).

appSpec.js

describe('checkForm', function() {

it('checkForm must be an object', function() {

expect(typeof checkForm).toBe('object');

});

});

Lanzamos el test y, claro está, falla porque aún no tenemos nada, así que preparamos nuestro objeto para superarlo.

app.js

var checkForm = {};

Ahora vamos a comprobar si tiene un método init, al que se invocará en el submit del formulario.

appSpec.js

...

it('checkForm has init method', function() {

expect(typeof checkForm.init).toBe('function');

});

...

El test falla, así que lo solucionamos añadiendo el método al objeto.

app.js

var checkForm = {

init: function(user, pass) {

}

};

A continuación, comprobamos si el objeto tiene un método para validar los campos que necesitamos y si este devuelve true cuando son correctos.

app.js

var user = 'foo@bar.com',

pass = 'bazinga';

it('checkForm has checkInputs method', function() {

expect(typeof checkForm.checkInputs).toBe('function');

});

it('inputs corrects are true', function() {

var inputs = checkForm.checkInputs(user, pass);

expect(inputs).toBeTruthy();

});

El test falla, así que lo solucionamos refactorizando el validador.

app.js

var checkForm = {

checkInputs: function(user, pass) {

var isValid = true;

/* ojo, así no se comprueba bien si es un correo válido, pero para esta demo nos vale */

if ( !user || !pass || user.indexOf('@') === -1 ) {

isValid = false;

}

return isValid;

},

init: function(user, pass) {

this.checkInputs(user, pass);

}

};

Ahora tocaría comprobar si da un resultado false cuando son inválidos, pero antes vamos a conocer otra funcionalidad de jasmine.

Antes y después

En el spec anterior hemos tenido que definir unas variables para ejecutar la prueba.  Se podría hacer así, seteándolas antes de cada spec, pero es mucho más limpio si tenemos controlados todos los mocks al principio de cada suite. Y para eso contamos con dos funciones que se ejecutan justo antes y después de cada spec: beforeEach y afterEach respectivamente, una acción que en argot se denomina setup y teardown. En nuestro caso, nos vale con resetear las dos variables después de cada spec.

describe('checkForm', function() {

var user = '',

pass = '';

afterEach(function() {

user = '',

pass = '';

});

...

De esta manera, aunque en una función modifiquemos su valor, para la siguiente vuelven a estar como al principio.

it('inputs corrects are true', function() {

var inputs;

user = 'foo@bar.com';

pass = 'bazinga';

inputs = checkForm.checkInputs(user, pass);

expect(inputs).toBeTruthy();

});

it('empty inputs are false', function() {

var inputs = checkForm.checkInputs(user, pass);

expect(inputs).toBeFalsy();

});

Bueno, quedan varias cosas más que comprobar en nuestra superaplicación, pero con lo expuesto espero que baste para entender cómo funciona en esencia la dinámica del TDD, que yo me voy de cenuki y a bailar 😛

el ejemplo completo está en git.

|| Tags: , , ,

valoración de los lectores sobre jasmine: 2. TDD y BDD

  • estrellica valoración positiva
  • estrellica valoración positiva
  • estrellica valoración positiva
  • estrellica valoración positiva
  • estrellica valoración positiva
  • 5 sobre 5 (3 votos)

¿Te ha parecido útil o interesante esta entrada?
dormido, valoración 1 nadapensativo, valoración 2 un poco sonrisa, valoración 3 a medias guiño, valoración 4 bastante aplauso, valoración 5 mucho

Tú opinión es muy importante, gracias por compartirla!

Aportar un comentario


*