logoCoderhouse.png
By Nicolas Alliaume • mayo 30, 2016

Cómo testear tu código en Javascript: una guía hacia la tranquilidad

Soy Nico, profe de coderhouse en Uruguay, y hoy les traigo un post para contarles cómo pueden testear que su código Javascript funciona correctamente usando Mocha.

¿Qué es Mocha?

Mocha es un framework de testing. Un framework es un conjunto de utilidades, clases, funciones que vienen empaquetadas para que nosotros los desarrolladores las utilicemos en nuestras aplicaciones.

Así como jQuery es un framework para Javascript que nos provee toda la manipulación del DOM (entre un montón de cosas más), Mocha es un framework que nos da un conjunto de funciones muy útiles para probar nuestro código Javascript.

Casos de Test

Cómo nos cuenta Félix en su post Testing: Introducción general, existen varios tipos de tests. En este artículo me voy a enfocar en los tests unitarios, los primeros que se crean, y que son escritos por los propios desarrolladores como una safety net para estar seguros que pueden seguir construyendo sobre una base sólida que funciona correctamente.

Algunas de las características más importantes de los tests unitarios son:

  • Cada test debe probar una, y solo una funcionalidad
  • Los tests son también código fuente. Se deben nombrar apropiadamente, documentar y mantener
  • Deben ser simples y centrados. Hacer tests por el hecho de hacerlos no agrega valor, sólo complejidad innecesaria
  • Deben ejecutarse rápido y sin intervención humana. Esto promueve que los desarrolladores ejecuten los tests con frecuencia luego de hacer modificaciones en el código, y que puedan automatizarse como parte de un proceso de desarrollo más automático (como en la integración continua).

¿En qué momento del ciclo de desarrollo escribimos los tests?

Esta pregunta tiene más de una respuesta posible. La escuela del TDD (Test Driven Development) mantiene que escribir los tests antes de desarrollar las funcionalidades ayuda a mantener el código simple, super testeable (volveré sobre este concepto en breve), y que desarrollaremos sólo lo necesario. La escuela tradicional dirá que los tests se escriben luego de escribir la funcionalidad, para probar si ésta funciona correctamente.

El concepto de testeablidad del código (qué tan testeable es) indica qué tan fácil es escribir tests para ese código. Esto es muy importante. Cuando nuestro código se empieza a complicar y hay más clases y funciones interrelacionadas, muchas veces se hace casi imposible escribir un test para probar cada una de las funcionalidades en forma independiente. Cuando esto ocurre decimos que el código es poco testeable. Como regla general, queremos que nuestro código sea lo más testeable posible, siempre y cuando ésto no signifique código super complejo; hay que poner todo en la balanza.

¿Qué aplica para un caso de test?

Los casos de test prueban una única funcionalidad. Obviamente evitamos probar cosas resueltas por el lenguaje de programación y los frameworks (en principio podemos asumir que las funciones de jQuery ya fueron testeadas y funcionan bien). Por ejemplo, no vamos a testear que la función sort() de Javascript efectivamente ordena un array de números. Pero si escribiéramos nuestra propia función de ordenamiento, entonces sería un claro ejemplo de una funcionalidad a testear.

Última escala antes de empezar: nombramiento y asserts

Voy a dedicar un párrafo para hablar del nombre de los casos de tests. Existen varias convenciones de nombres para los tests unitarios. Mi favorita (en inglés) es la siguiente:

  • Los nombres de cada test comienzan en should
  • El nombre describe lo que debería ocurrir si la funcionalidad está bien y el contexto en que se ejecuta

Por ejemplo, si vamos a probar nuestra función customizada de ordenamiento de números, deberíamos crear varios tests, de los cuales uno será shouldOrderAscendentDifferentUnorderedIntegers(). Por lo general los nombres de los tests serán largos (incluso más que el del ejemplo). Esto no es algo malo, todo lo contrario. Si el test falla, el nombre del test que falla será una valiosísima pista para saber qué es lo que no está funcionando bien.

Los asserts son un conjunto de funciones que utilizamos para hacer la verificación de los resultados de las funcionalidades que estamos probando. Existe un assert para verificar valores verdadero o falso, para verificar que dos valores sean iguales, que dos arrays contienen los mismos elementos, que un valor no es null, entre otros. Todo caso de test unitario cuenta con al menos un assert que verifica el resultado.

Ahora si, ¡a testear!

Utilizaré NodeJS para correr el código y los tests. Mi colega Oscar nos dejó un post sobre cómo crear una aplicación en tiempo real usando NodeJS que puede ser de utilidad si nunca estuviste en contacto antes con Node.

Instalar Mocha

Cuando tenemos NodeJS instalado, se instala también una utilidad llamada npm, un gestor de paquetes que nos permite instalar módulos por línea de comandos bien sencillo. Para nuestro ejemplo necesitaremos instalar Mocha (el framework de testing) y chai. Posicionados en la línea de comandos (terminal) en una carpeta nueva para nuestro mini-proyecto (que llamaré mi-proyecto), instalamos todo corriendo los siguientes comandos (si estás en linux o mac puede ser necesario que lo hagas con sudo):

<code>npm install -g mocha</code>

También utilizaremos chai para incorporar asserts prácticos:

<code>npm install chai</code>

Primer caso de test

Comencemos con un código en Javascript de ejemplo. Voy a utilizar algunos fragmentos del TA-TE-TI de uno de mis posts anteriores: Creando un TA-TE-TI web en Javascript con un jugador artificial.

La primer función que vamos a testear es la siguiente:

// True si las celdas de la fila indicada son iguales y no nulas
function tatetiRow(row, board) {
return board[row][0] != null
&& (board[row][0] == board[row][1] && board[row][1] == board[row][2]);
}

exports.tatetiRow = tatetiRow;

Esta función retorna true si hay TA-TE-TI en una fila dada del tablero. Para esta función escribiremos 3 tests, uno con TA-TE-TI y dos sin TA-TE-TI.

Colocaré el código de la función en un archivo llamado functions.js, y crearé mi test en uno llamado unit-tests.js dentro de una carpeta llamada test. Así:

mi-proyecto
|__ functions.js
|__ test
|__ unit-tests.js

En unit-tests.js pondremos nuestros primeros casos de test:

var assert = require('chai').assert;
var tateti = require('../functions');

describe('TATETI', function() {

describe('#rows', function () {

it('should return true for TATETI when all columns are equal and not null', function() {
var board = [
[2,2,2],
[null,1,null],
[null,1,1]
];
assert(tateti.tatetiRow(0, board));
});

});
});

El primer describe define una agrupación a alto nivel, en este caso, nuestro juego de TATETI (podríamos llamarle el módulo). El segundo describe define una agrupación a nivel de funcionalidad (en este caso, determinar TATETI en las filas). Dentro de este último describe tenemos un it, que define un caso de test. En este caso, el test verifica que la función retorna True cuando hay TATETI en la fila indicada.

Ahora, ¡vamos a correr el test a ver que pasa!

Para ejecutar el caso de test, escribimos en la terminal:

<code>mocha</code>

Para que funcione debemos estar posicionados en la carpeta del proyecto (en el ejemplo, mi-proyecto).

El comando mocha imprime lo siguiente:

Resultado del primer test

¡Funcionó!

Agregando nuevos casos de test

Incorporamos los otros dos casos de test que comentaba antes para verificar los casos en que no debe dar True.

Recordá que cada caso de test es un it dentro del describe de adentro. El código completo con los 3 casos de test se ve asi:

var assert = require('chai').assert;
var tateti = require('../functions');

describe('TATETI', function() {

describe('#rows', function () {

it('should return true for TATETI when all columns are equal and not null', function() {
var board = [
[2,2,2],
[null,1,null],
[null,1,1]
];
assert(tateti.tatetiRow(0, board));
});

it('should return false for TATETI when all columns are different', function() {
var board = [
[2,2,2],
[null,1,null],
[null,1,1]
];
assert(false === tateti.tatetiRow(2, board)); // se puede escribir asi...
});

it('should return false for TATETI when all columns are equal but null', function() {
var board = [
[null,null,null],
[null,1,null],
[null,1,1]
];
assert(!tateti.tatetiRow(0, board)); // ..., o también asi
});

});

});

Lo corremos, y….

Test de tres funciones de TATETI

¡Perfecto!

Hagamos una función más

Vamos a testear una función que retorna un array. Agreguemos la siguiente función a functions.js:

// Devuelve las coordenadas como [fila,columna] de las posiciones
// libres del tablero
function getFreePositions(board) {
var freePositions = [];
for (var row = 0; row < 3; row++) {
for (var col = 0; col < 3; col++) {
if (board[row][col] === null) {
freePositions.push([row, col]);
}
}
}
return freePositions;
}

exports.getFreePositions = getFreePositions;

Esta función retorna todas las coordenadas en el tablero que están vacías. Es vacío cuando su valor es null.

Ahora, el caso de test. Para esto vamos a crear otro describe con sus casos de test dentro de unit-tests.js, así:


var assert = require('chai').assert;
var tateti = require('../functions');

describe('TATETI', function() {

describe('#rows', function () {

it('should return true for TATETI when all columns are equal and not null', function() {
var board = [
[2,2,2],
[null,1,null],
[null,1,1]
];
assert(tateti.tatetiRow(0, board));
});

it('should return false for TATETI when all columns are different', function() {
var board = [
[2,2,2],
[null,1,null],
[null,1,1]
];
assert(false === tateti.tatetiRow(2, board)); // se puede escribir asi...
});

it('should return false for TATETI when all columns are equal but null', function() {
var board = [
[null,null,null],
[null,1,null],
[null,1,1]
];
assert(!tateti.tatetiRow(0, board)); // ..., o también asi
});

});

describe('#empty-cells', function() {

it('should return all cells when they are all empty', function() {
var board = [
[null,null,null],
[null,null,null],
[null,null,null]
];
var expected = [[0,0],[0,1],[0,2],[1,0],[1,1],[1,2],[2,0],[2,1],[2,2]];
assert.sameDeepMembers(expected, tateti.getFreePositions(board));
});

it('should return an empty array when all cells are not null', function() {
var board = [
[1,2,1],
[2,1,2],
[2,1,2]
];
assert.lengthOf(tateti.getFreePositions(board), 0);
});

it('should return an array with only the positions of null values', function() {
var board = [
[1,null,1],
[2,1,null],
[null,1,2]
];
var expected = [[0,1],[1,2],[2,0]];
assert.sameDeepMembers(expected, tateti.getFreePositions(board));
});

});

});

Ahora, corremos los tests, y…

Resultado final de los tests

¡Un éxito! Ya estamos seguros que nuestras funciones se comportan como deberían. Podemos seguir construyendo nuestra aplicación tranquilos.

Comentarios finales

Tener tests unitarios nos da la tranquilidad de desarrollar sobre código que sabemos que funciona, y poder modificar nuestro código tranquilos, sabiendo que podemos volver a correr los tests y comprobar que todo sigue bien.

Te invito a que empieces a incorporar de a poco los tests unitarios a tu desarrollo, ¡vas a ver que tranquilo que dormís por la noche! Al principio te va a parecer muy engorroso, quizás una pérdida de tiempo. ¡Pero todo cambia la primera vez que encontras los bugs antes de entregar tu código!