Tutorial

Cómo probar un módulo de Node.js con Mocha y Assert

Node.jsDevelopmentJavaScript

El autor seleccionó a Open Internet/Free Speech Fund para recibir una donación como parte del programa Write for DOnations.

Introducción

Las pruebas son una parte integral del desarrollo de software. Es común para los programadores ejecutar código para probar su aplicación a medida que realizan cambios y así confirmar que se se comporta como se espera. Con la configuración de prueba correcta, este proceso puede incluso automatizarse y ahorrar mucho tiempo. Ejecutar pruebas de manera uniforme tras escribir nuevo código garantiza que los nuevos cambios no afecten las funciones preexistentes. Esto proporciona al desarrollador confianza en su base de código, en especial cuando se implementa en la producción para que los usuarios puedan interactuar con ella.

Un marco de prueba estructura la forma en que creamos casos de prueba. Mocha es un marco de prueba de JavaScript muy popular que organiza nuestros casos de prueba y se encarga de ejecutarlos. Sin embargo, no verifica el comportamiento de nuestro código. Para comparar los valores en una prueba, podemos usar el módulo assert de Node.js.

En este artículo, escribirá pruebas para un módulo de lista TODO de Node.js. Configurará y usará el marco de prueba Mocha para estructurar sus pruebas. A continuación, usará el módulo assert Node.js para crear las pruebas propiamente dichas. En este sentido, usará Mocha como creador del plan, y assert para implementar el plan.

Requisitos previos

Paso 1: Escribir un módulo Node

Nuestro primer paso para este artículo será escribir el módulo Node.js que nos gustaría probar. Este módulo administrará una lista de elementos TODO. Con este módulo podremos enumerar todos los elementos TODO de los que realizamos un seguimiento, añadir nuevos elementos y marcar algunos como completados. Además, podrá exportar una lista de elementos TODO a un archivo CSV. Si desea repasar la manera de escribir módulos de Node.js, puede leer nuestro artículo Cómo crear un módulo de Node.js.

Primero, debemos configurar el entorno de codificación. Cree una carpeta con el nombre de su proyecto en su terminal. En este tutorial se usará el nombre todos:

  • mkdir todos

Luego acceda a esa carpeta:

  • cd todos

Ahora, inicie npm. Usaremos su funcionalidad CLI para ejecutar las pruebas más tarde:

  • npm init -y

Solo tenemos una dependencia, Mocha, que usaremos para organizar y ejecutar nuestras pruebas. Para descargar e instalar Mocha, utilice lo siguiente:

  • npm i request --save-dev mocha

Instalamos Mocha como dependencia dev, ya que en un entorno de producción el módulo no lo necesita. Si desea obtener más información sobre los paquetes de Node.js o npm, consulte nuestra guía Cómo usar módulos Node.js con npm y package.json.

Finalmente, crearemos nuestro archivo que contendrá el código de nuestro módulo:

  • touch index.js

Con esto, estaremos listos para crear nuestro módulo. Abra index.js en un editor de texto; por ejemplo, nano:

  • nano index.js

Comenzaremos definiendo la clase Todos. Esta clase contiene todas las funciones que necesitamos para administrar nuestra lista TODO. Añada las siguientes líneas de código a index.js:

todos/index.js
class Todos {
    constructor() {
        this.todos = [];
    }
}

module.exports = Todos;

Comenzamos el archivo creando una clase Todos. Su función constructor() no toma argumentos. Por lo tanto, no es necesario proporcionar valores para crear instancias de un objeto para esta clase. Lo único que hacemos cuando iniciamos un objeto Todos es crear una propiedad todos, que es una matriz vacía.

La línea modules permite que otros módulos de Node.js requieran nuestra clase Todos. Si no se exporta explícitamente la clase, el archivo de prueba que crearemos más tarde no podrá usarla.

Añadiremos una función para mostrar la matriz de todos que almacenamos. Escriba las siguientes líneas resaltadas:

todos/index.js
class Todos {
    constructor() {
        this.todos = [];
    }

    list() {
        return [...this.todos];
    }
}

module.exports = Todos;

Nuestra función list() devuelve una copia de la matriz usada por la clase. Hace una copia de la matriz usando la sintaxis de desestructuración de JavaScript. Creamos una copia de la matriz de modo que los cambios que el usuario realice en la matriz y se muestren a través de list() no afecten a la matriz usada por el objeto Todos.

Nota: Las matrices de JavaScript son tipos de referencia. Esto significa que para cualquier asignación de variable a una matriz o invocación de función con una matriz como parámetro, JavaScript consulta la matriz original que se creó. Por ejemplo, si tenemos una matriz con tres elementos llamada x, y creamos una nueva variable y de modo que y = x, y y x hacen referencia a lo mismo. Cualquier cambio que realicemos a la matriz con y afecta a la variable x y viceversa.

Ahora escribiremos la función add(), que añade un nuevo elemento TODO:

todos/index.js
class Todos {
    constructor() {
        this.todos = [];
    }

    list() {
        return [...this.todos];
    }

    add(title) {
        let todo = {
            title: title,
            completed: false,
        }

        this.todos.push(todo);
    }
}

module.exports = Todos;

Nuestra función add() toma una cadena y la dispone en una nueva propiedad title del objeto de JavaScript. El nuevo objeto también tiene una propiedad completed, que se fija en false por defecto. Luego añadimos este nuevo objeto a nuestra matriz de TODO.

Una funcionalidad importante en un administrador de TODO es la de marcar los elementos como completados. Para esta implementación, repetiremos nuestra matriz todos para hallar el elemento TODO que el usuario está buscando. Si se encuentra uno, lo marcaremos como completado. Si no se encuentra ninguno, mostraremos un error.

Añada la función complete() de esta forma:

todos/index.js
class Todos {
    constructor() {
        this.todos = [];
    }

    list() {
        return [...this.todos];
    }

    add(title) {
        let todo = {
            title: title,
            completed: false,
        }

        this.todos.push(todo);
    }

    complete(title) {
        let todoFound = false;
        this.todos.forEach((todo) => {
            if (todo.title === title) {
                todo.completed = true;
                todoFound = true;
                return;
            }
        });

        if (!todoFound) {
            throw new Error(`No TODO was found with the title: "${title}"`);
        }
    }
}

module.exports = Todos;

Guarde el archivo y cierre el editor de texto.

Ahora disponemos de un administrador TODO básico con el que podemos experimentar. A continuación, probaremos nuestro código manualmente para ver si la aplicación funciona.

Paso 2: Probar el código de forma manual

En este paso, ejecutaremos las funciones de nuestro código y observaremos el resultado para garantizar que cumpla con nuestras expectativas. Esto se conoce como prueba manual. Es probablemente la metodología de prueba más común que aplican los programadores. Aunque automatizaremos nuestra prueba más tarde con Mocha, primero probaremos manualmente nuestro código para dar un mejor sentido a cómo la prueba manual difiere de los marcos de prueba.

Añadiremos dos elementos TODO a nuestra aplicación y marcaremos uno como completado. Inicie el REPL de Node.js en la misma carpeta que el archivo index.js:

  • node

Verá el símbolo > en el REPL que dice que podemos ingresar código de JavaScript. Escriba lo siguiente en el símbolo:

  • const Todos = require('./index');

Con require(), cargamos el módulo TODOs en una variable Todos. Recuerde que nuestro módulo muestra la clase Todos por defecto.

Ahora, vamos a instanciar un objeto para esa clase. En el REPL, añada esta línea de código:

  • const todos = new Todos();

Podemos usar el objeto todos para verificar que nuestra implementación funcione. Añadiremos nuestro primer elemento TODO:

  • todos.add("run code");

Hasta ahora, no vimos ningún resultado en nuestro terminal. Verificaremos que almacenamos nuestro elemento TODO "run code" obteniendo una lista de todos nuestros TODO:

  • todos.list();

Verá este resultado en su REPL:

Output
[ { title: 'run code', completed: false } ]

Este es el resultado esperado: tenemos un elemento TODO en nuestra matriz de TODO y no está completado por defecto.

Vamos añadiremos otro elemento TODO:

  • todos.add("test everything");

Marque el primer elemento TODO como completado:

  • todos.complete("run code");

Nuestro objeto todos ahora estará administrando dos elementos: "run code" y "test everything". El TODO "run code" también se completará. Confirmaremos esto invocando list() de nuevo:

  • todos.list();

El REPL mostrará el siguiente resultado:

Output
[ { title: 'run code', completed: true }, { title: 'test everything', completed: false } ]

Ahora, salga del REPL con lo siguiente:

  • .exit

Hemos confirmado que nuestro módulo se comporta como esperamos. Aunque no dispusimos nuestro código en un archivo de prueba o usamos una biblioteca de prueba, probamos nuestro código manualmente. Desafortunadamente, esta forma de prueba requiere mucho tiempo cada vez que se realiza un cambio. A continuación, utilizaremos las pruebas automatizadas en Node.js y veremos si podemos resolver este problema con el marco de prueba Mocha.

Paso 3: Escribir su primera prueba con Mocha y Assert

En el último paso, probamos nuestra aplicación de forma manual. Esto funcionará para los casos de uso individuales, pero a medida que nuestro módulo se amplía, este método se vuelve menos viable. A medida que probemos nuevas funciones, debemos estar seguros de que la funcionalidad añadida no genere problemas con la funcionalidad anterior. Nos gustaría probar cada función una y otra vez para cada cambio del código, pero hacer esto a mano tomaría mucho esfuerzo y podría producir a errores.

Una práctica más eficiente consistiría en configurar pruebas automatizadas. Estas son pruebas con secuencias de comandos escritas como cualquier otro bloque de código. Ejecutamos nuestras funciones con entradas definidas e inspeccionamos sus efectos para garantizar que se comporten como esperamos. A medida que nuestra base de código se amplíe, lo mismo sucederá con nuestras pruebas automatizadas. Cuando escribimos nuevas pruebas junto a las funciones, podemos verificar que el módulo completo aún funcione, todo sin necesidad de recordar la manera en que se usa cada función en cada ocasión.

En este tutorial, usaremos el marco de pruebas Mocha con el módulo assert de Node.js. Recurriremos a la experiencia práctica para ver cómo funcionan juntos.

Para comenzar, cree un nuevo archivo para almacenar nuestro código de prueba:

  • touch index.test.js

Ahora, utilice su editor de texto para abrir el archivo de prueba. Puede usar nano como antes:

  • nano index.test.js

En la primera línea del archivo de texto, cargaremos el módulo TODO como hicimos en el shell de Node.js. Luego cargaremos el módulo assert para cuando escribamos nuestras pruebas. Añada las siguientes líneas:

todos/index.test.js
const Todos = require('./index');
const assert = require('assert').strict;

La propiedad strict del módulo assert nos permitirá usar las pruebas de igualdad especiales que se recomiendan desde Node.js y son adecuadas para una buena protección futura, ya que tienen en cuenta la mayoría de los casos de uso.

Antes de pasar a las pruebas de escritura, veremos cómo Mocha organiza nuestro código. Las pruebas estructuradas en Mocha normalmente siguen esta plantilla:

describe([String with Test Group Name], function() {
    it([String with Test Name], function() {
        [Test Code]
    });
});

Observe las dos funciones principales: describe() e it(). La función describe() se usa para agrupar pruebas similares. No es necesario para que Mocha ejecute las pruebas, pero agrupar las pruebas facilita el mantenimiento de nuestro código. Se recomienda que agrupe sus pruebas de una manera que le permita actualizar fácilmente las que son similares.

it() contiene nuestro código de prueba. Aquí interactuaremos con las funciones de nuestro módulo y usaremos la bibliteca assert. Muchas funciones it() pueden definirse en una función describe().

Nuestro objetivo en esta sección es usar Mocha y assert para automatizar nuestra prueba manual. Haremos esto paso a paso, comenzando con nuestro bloque de descripción. Añada lo siguiente a su archivo después de las líneas del módulo:

todos/index.test.js
...
describe("integration test", function() {
});

Con este bloque de código, hemos creado una agrupación para nuestras pruebas integradas. Las pruebas Unit probarán una función a la vez. Las pruebas de integración verifican si las funciones en o entre los módulos funcionan bien juntas. Cuando Mocha ejecute nuestras pruebas, todas las pruebas en ese bloque de descripción se ejecutarán en el grupo "integration test".

Añadiremos una función it() para que podamos comenzar a probar el código de nuestro módulo.

todos/index.test.js
...
describe("integration test", function() {
    it("should be able to add and complete TODOs", function() {
    });
});

Observe que hicimos que el nombre de la prueba sea descriptivo. Si alguien ejecuta nuestra prueba, quedará claro de inmediato qué supera la prueba y qué no. Una aplicación bien probada normalmente es una aplicación bien documentada, y las pruebas pueden ser a veces un tipo de documentación efectivo.

Para nuestra primera prueba, crearemos un nuevo objeto Todos y verificaremos que no tenga elementos.

todos/index.test.js
...
describe("integration test", function() {
    it("should be able to add and complete TODOs", function() {
        let todos = new Todos();
        assert.notStrictEqual(todos.list().length, 1);
    });
});

La primera nueva línea de código creó una instancia de un nuevo objeto Todos como haríamos en el REPL de Node.js o en otro módulo. En la segunda nueva línea, usamos el módulo assert.

Desde el módulo assert usamos el método notStrictEqual(). Esta función toma dos parámetros: el valor que deseamos probar (llamado actual) y el valor que esperamos obtener (llamado expected). Si ambos argumentos son iguales, notStrictEqual() genera un error para que la prueba falle.

Guarde y cierre index.test.js.

El caso básico será true, ya que la extensión debería ser 0, que no es 1. Vamos a confirmar esto ejecutando Mocha. Para hacer esto, debemos modificar nuestro archivo package.json. Abra el archivo package.json con su editor de texto:

  • nano package.json

Ahora, en su propiedad scripts, cámbielo para que tenga este aspecto:

todos/package.json
...
"scripts": {
    "test": "mocha index.test.js"
},
...

Acabamos de cambiar el comportamiento del comando CLI test de npm. Cuando ejecutemos npm test, npm revisará el comando que acabamos de introducir en package.json. Buscará la biblioteca Mocha en nuestra carpeta node_modules y ejecutará el comando mocha con nuestro archivo de prueba.

Guarde y cierre package.json.

Veamos qué sucede cuando ejecutemos nuestra prueba. En su terminal, introduzca lo siguiente:

  • npm test

El comando producirá el siguiente resultado:

Output
> todos@1.0.0 test your_file_path/todos > mocha index.test.js integrated test ✓ should be able to add and complete TODOs 1 passing (16ms)

Este resultado primero nos muestra el grupo de pruebas que está a punto de ejecutarse. Para cada prueba individual en un grupo, se marca el caso de prueba. Vemos el nombre de nuestra prueba como lo describimos en la función it(). La marca de verificación en el lado izquierdo del caso de prueba indica que se superó la prueba.

En la parte inferior, vemos un resumen de todas nuestras pruebas. En nuestro caso, nuestra prueba fue superada y se completó en 16 ms (el tiempo varía según el equipo).

Nuestra prueba se inició correctamente. Sin embargo, este caso de prueba actual puede dar lugar a instancias de “falsos positivos”. Un falso positivo es un caso en el que se supera la prueba cuando esto no debería suceder.

Actualmente comprobamos que la extensión de la matriz no sea igual a 1. Modificaremos la prueba de modo que esta condición sea “true” cuando este no debería ser el caso. Añada las siguientes líneas a index.test.js:

todos/index.test.js
...
describe("integration test", function() {
    it("should be able to add and complete TODOs", function() {
        let todos = new Todos();
        todos.add("get up from bed");
        todos.add("make up bed");
        assert.notStrictEqual(todos.list().length, 1);
    });
});

Guarde el archivo y ciérrelo.

Añadimos dos elementos TODO. Ejecutaremos la prueba para ver qué sucede:

  • npm test

Esto dará el siguiente resultado:

Output
... integrated test ✓ should be able to add and complete TODOs 1 passing (8ms)

La prueba se supera, como se espera, ya que la extensión es mayor a 1. Sin embargo, se frustra la finalidad original de contar con esa primera prueba. La primera prueba está destinada a confirmar que empezamos en un estado vacío. Una mejor prueba confirmará eso en todos los casos.

Cambiaremos la prueba de modo que solo se supere si no tenemos ningú´n TODO. Aplique los siguientes cambios en index.test.js:

todos/index.test.js
...
describe("integration test", function() {
    it("should be able to add and complete TODOs", function() {
        let todos = new Todos();
        todos.add("get up from bed");
        todos.add("make up bed");
        assert.strictEqual(todos.list().length, 0);
    });
});

Cambió notStrictEqual() por strictEqual(), una función que comprueba la igualdad entre su argumento real y esperado. “strictEqual” fallará si nuestros argumentos no son exactamente los mismos.

Guarde y cierre el archivo, y ejecute la prueba para que podamos ver lo que sucede:

  • npm test

Esta vez, en el resultado se mostará un error:

Output
... integration test 1) should be able to add and complete TODOs 0 passing (16ms) 1 failing 1) integration test should be able to add and complete TODOs: AssertionError [ERR_ASSERTION]: Input A expected to strictly equal input B: + expected - actual - 2 + 0 + expected - actual -2 +0 at Context.<anonymous> (index.test.js:9:10) npm ERR! Test failed. See above for more details.

Este texto nos resultará útil para determinar por qué falló la prueba mediante depuración. Observe que, dado que la prueba falló, no apareció una marca de verificación al principio del caso de prueba.

Nuestro resumen de prueba ya no está en la parte inferior del resultado, sino justo después de donde nuestra lista de casos de prueba se mostraba:

...
0 passing (29ms)
  1 failing
...

El resultado restante nos proporciona datos sobre las pruebas no superadas. Primero, vemos el caso de prueba que falló:

...
1) integrated test
       should be able to add and complete TODOs:
...

A continuación, vemos por qué falló nuestra prueba:

...
      AssertionError [ERR_ASSERTION]: Input A expected to strictly equal input B:
+ expected - actual

- 2
+ 0
      + expected - actual

      -2
      +0

      at Context.<anonymous> (index.test.js:9:10)
...

Se presenta un AssertionError cuando strictEqual() falla. Vemos que el valor expected, 0, es diferente el valor actual.

A continuación vemos la línea en nuestro archivo de prueba en el que falla el código. En este caso, es la línea 10.

Ahora, vimos que nuestra prueba fallará si esperamos valores incorrectos. Cambiaremos nuestro caso de prueba de nuevo a su valor correcto. Primero, abra el archivo:

  • nano index.test.js

A continuación, elimine las líneas todos.add de modo que su código tenga el siguiente aspecto:

todos/index.test.js
...
describe("integration test", function () {
    it("should be able to add and complete TODOs", function () {
        let todos = new Todos();
        assert.strictEqual(todos.list().length, 0);
    });
});

Guarde el archivo y ciérrelo.

Ejecútelo una vez más para confirmar que supere la prueba sin posibles instancias de “falsos positivos”:

  • npm test

El resultado será el siguiente:

Output
... integration test ✓ should be able to add and complete TODOs 1 passing (15ms)

Con esto, mejoramos bastante la resistencia de nuestra prueba. Continuaremos con nuestra prueba de integración. El siguiente paso es añadir un nuevo elemento TODO a index.test.js:

todos/index.test.js
...
describe("integration test", function() {
    it("should be able to add and complete TODOs", function() {
        let todos = new Todos();
        assert.strictEqual(todos.list().length, 0);

        todos.add("run code");
        assert.strictEqual(todos.list().length, 1);
        assert.deepStrictEqual(todos.list(), [{title: "run code", completed: false}]);
    });
});

Tras utilizar la función add(), confirmamos que ahora tenemos un TODO administrado por nuestro objeto todos con strictEqual(). Nuestra siguiente prueba confirma los datos en todos con deepStrictEqual(). La función deepStrictEqual() prueba de forma recursiva que nuestros objetos esperados y reales tienen las mismas propiedades. En este caso, prueba que las matrices que esperamos tengan un objeto JavaScript en ellas. Luego comprueba que sus objetos JavaScript tengan las mismas propiedades; es decir, que sus propiedades titles sean "run code" y sus propiedades completed sean false.

A continuación, completaremos las pruebas restantes con estas dos comprobaciones de igualdad, según sea necesario, añadiendo las siguientes líneas resaltadas:

todos/index.test.js
...
describe("integration test", function() {
    it("should be able to add and complete TODOs", function() {
        let todos = new Todos();
        assert.strictEqual(todos.list().length, 0);

        todos.add("run code");
        assert.strictEqual(todos.list().length, 1);
        assert.deepStrictEqual(todos.list(), [{title: "run code", completed: false}]);

        todos.add("test everything");
        assert.strictEqual(todos.list().length, 2);
        assert.deepStrictEqual(todos.list(),
            [
                { title: "run code", completed: false },
                { title: "test everything", completed: false }
            ]
        );

        todos.complete("run code");
        assert.deepStrictEqual(todos.list(),
            [
                { title: "run code", completed: true },
                { title: "test everything", completed: false }
            ]
    );
  });
});

Guarde el archivo y ciérrelo.

Nuestra prueba ahora imita a nuestra prueba manual. Con estas pruebas mediante programación, no es necesario comprobar el resultado continuamente si nuestras pruebas son superadas cuando las ejecutamos. Normalmente, le convendrá probar todos los aspectos de uso para garantizar que el código se pruebe adecuadamente.

Ejecutaremos nuestra prueba con npm test una vez más para obtener este resultado familiar:

Output
... integrated test ✓ should be able to add and complete TODOs 1 passing (9ms)

Con esto, habrá configurado una prueba integrada con el marco Mocha y la biblioteca assert.

Consideraremos una situación en la que haya compartido nuestro módulo con otros desarrolladores y ahora nos den su opinión. Una buena parte de nuestros usuarios desearán que la función complete() muestre un error si no se añadieron TODO aún. Añadiremos esta funcionalidad en nuestra función complete().

Abra index.js en su editor de texto:

  • nano index.js

Añada lo siguiente a la función:

todos/index.js
...
complete(title) {
    if (this.todos.length === 0) {
        throw new Error("You have no TODOs stored. Why don't you add one first?");
    }

    let todoFound = false
    this.todos.forEach((todo) => {
        if (todo.title === title) {
            todo.completed = true;
            todoFound = true;
            return;
        }
    });

    if (!todoFound) {
        throw new Error(`No TODO was found with the title: "${title}"`);
    }
}
...

Guarde el archivo y ciérrelo.

Ahora, añadiremos una nueva prueba para esta nueva función. Queremos verificar que si invocamos “completed” en un objeto Todos que no tiene elementos, mostrará nuestro error especial.

Vuelva a index.test.js:

  • nano index.test.js

Al final del archivo, añada el siguiente código:

todos/index.test.js
...
describe("complete()", function() {
    it("should fail if there are no TODOs", function() {
        let todos = new Todos();
        const expectedError = new Error("You have no TODOs stored. Why don't you add one first?");

        assert.throws(() => {
            todos.complete("doesn't exist");
        }, expectedError);
    });
});

Usamos describe() y it() como antes. Nuestra prueba comienza con la creación de un nuevo objeto todos. A continuación, definimos el error que estamos esperando ver cuando invocamos la función complete().

A continuación, usamos la función throws() del módulo assert. Esta función se creó para que podamos verificar los errores que se producen en nuestro código. Su primer argumento es una función que contiene el código que produce el error. El segundo argumento es el error que estamos esperando ver.

En su terminal, ejecute las pruebas con npm test de nuevo. Ahora verá el siguiente resultado:

Output
... integrated test ✓ should be able to add and complete TODOs complete() ✓ should fail if there are no TODOs 2 passing (25ms)

En este resultado se resalta el beneficio que nos trae realizar pruebas automatizadas con Mocha y assert. Debido a que nuestras pruebas están programadas, cada vez que ejecutamos npm test verificamos que todas nuestras pruebas se superan. No fue necesario comprobar manualmente si el otro código aún funciona; sabemos que es así porque las pruebas que tenemos se han superado.

Hasta ahora, nuestras pruebas han verificado los resultados del código sincronizado. Vamos a ver cómo deberíamos adaptar nuestros nuevos hábitos de prueba para que funcionen con el código asíncrono.

Paso 4: Probar código asíncrono

Una de las funciones que deseamos en nuestro módulo TODO es una función de exportación de CSV. Imprimirá en un archivo todos los TODO que tenemos guardados junto con el estado “completed”. Esto requiere que usemos el módulo fs: un módulo de Node.js integrado para trabajar con el sistema de archivos.

La escritura en un archivo es una operación asíncrona. Existen muchas formas de escribir en un archivo en Node.js. Podemos usar callbacks, promesas o las palabras claves async/await. En esta sección, veremos la manera de escribir pruebas para esos métodos diferentes.

Callbacks

Una función callback es la que se utiliza como argumento para una función asíncrona. Se invoca cuando se completa la operación asíncrona.

Añadiremos una función a nuestra clase Todos llamada saveToFile(). Esta función creará una cadena realizando un bucle con todos nuestros elementos TODO y escribiendo esa cadena en un archivo.

Abra su archivo index.js:

  • nano index.js

En este archivo, añada el siguiente código resaltado:

todos/index.js
const fs = require('fs');

class Todos {
    constructor() {
        this.todos = [];
    }

    list() {
        return [...this.todos];
    }

    add(title) {
        let todo = {
            title: title,
            completed: false,
        }
        this.todos.push(todo);
    }

    complete(title) {
        if (this.todos.length === 0) {
            throw new Error("You have no TODOs stored. Why don't you add one first?");
        }

        let todoFound = false
        this.todos.forEach((todo) => {
            if (todo.title === title) {
                todo.completed = true;
                todoFound = true;
                return;
            }
        });

        if (!todoFound) {
            throw new Error(`No TODO was found with the title: "${title}"`);
        }
    }

    saveToFile(callback) {
        let fileContents = 'Title,Completed\n';
        this.todos.forEach((todo) => {
            fileContents += `${todo.title},${todo.completed}\n`
        });

        fs.writeFile('todos.csv', fileContents, callback);
    }
}

module.exports = Todos;

Primero tenemos que importar el módulo fs en nuestro archivo. Luego añadimos nuestra nueva función saveToFile(). Nuestra función toma una función callback que se utilizará una vez que se complete la operación de escritura del archivo. En esa función, creamos una variable fileContents que almacena toda la cadena que queremos guardar como archivo. Se inicializa con los encabezados de CSV. A continuación, realizamos un bucle con cada elemento TODO con el método forEach() de la matriz interna. A medida que realizamos iteraciones, añadimos las propiedades title y completed de los objetos todos individuales.

Finalmente, usamos el módulo fs para escribir el archivo con la función writeFile(). Nuestro primer argumento es el nombre del archivo: todos.csv. El segundo es el contenido del archivo. En este caso, nuestra variable fileContents. Nuestro último argumento es nuestra función callback, que gestiona cualquier error de escritura.

Guarde el archivo y ciérrelo.

Ahora escribiremos una prueba para nuestra función saveToFile(). Nuestra prueba hará dos acciones: confirmar que el archivo existe en primer lugar y luego verificar que su contenido sea correcto.

Abra el archivo index.test.js:

  • nano index.test.js

Comenzaremos cargando el módulo fs en la parte superior del archivo, ya que lo usaremos para ayudar a probar nuestros resultados:

todos/index.test.js
const Todos = require('./index');
const assert = require('assert').strict;
const fs = require('fs');
...

Ahora, al final del archivo, añadiremos nuestro caso de prueba:

todos/index.test.js
...
describe("saveToFile()", function() {
    it("should save a single TODO", function(done) {
        let todos = new Todos();
        todos.add("save a CSV");
        todos.saveToFile((err) => {
            assert.strictEqual(fs.existsSync('todos.csv'), true);
            let expectedFileContents = "Title,Completed\nsave a CSV,false\n";
            let content = fs.readFileSync("todos.csv").toString();
            assert.strictEqual(content, expectedFileContents);
            done(err);
        });
    });
});

Como antes, usamos describe() para agrupar nuestras pruebas por separado respecto de las otras, ya que implica una nueva funcionalidad. La función it() se diferencia ligeramente de las otras. Normalmente, la función callback que usamos no tiene argumentos. Esta vez, tenemos done como argumento. Necesitamos este argumento cuando probemos funciones con callbacks. Mocha utiliza la función de callback done() para indicarle cuando se completa una función asíncrona.

Todas las funciones de callback probadas en Mocha deben invocar el callback done(). Si no es así, Mocha nunca sabría cuándo se completó la función y se quedaría esperando una señal.

Para continuar, creamos nuestra instancia de Todos y añadimos un único elemento a ella. Luego, invocamos la función saveToFile() con un callback que captura un error de escritura de archivo. Observe que nuestra prueba para esta función reside en el callback. Si nuestro código de prueba estuviera fuera del callback, fallaría siempre que se invocase antes de completar la escritura del archivo.

En nuestra función de callback, primero comprobamos que nuestro archivo existe:

todos/index.test.js
...
assert.strictEqual(fs.existsSync('todos.csv'), true);
...

La función fs.existsSync() muestra true si la ruta del archivo en su argumento existe; de lo contrario, será false.

Nota: Las funciones del módulo fs son asíncronas por defecto. Sin embargo, para las funciones claves, crean equivalentes síncronos. Esta prueba es más sencilla al usar las funciones síncronas, ya que no necesitamos anidar el código asíncrono para garantizar que funcione. En el módulo fs, las funciones síncronas normalmente terminan con "Sync" al final de sus nombres.

A continuación, crearemos una variable para guardar nuestro valor esperado:

todos/index.test.js
...
let expectedFileContents = "Title,Completed\nsave a CSV,false\n";
...

Usamos readFileSync() del módulo fs para leer el archivo de forma síncrona:

todos/index.test.js
...
let content = fs.readFileSync("todos.csv").toString();
...

Ahora, proporcionamos readFileSync() con la ruta correcta para el archivo: todos.csv. A medida que readFileSync() muestre el objeto Buffer, que almacena datos binarios, usamos su método toString() para poder comparar su valor con la cadena que esperamos tener guardada.

Como antes, usamos el strictEqual del módulo assert para realizar una comparación:

todos/index.test.js
...
assert.strictEqual(content, expectedFileContents);
...

Terminamos nuestra prueba invocando el callback done() y garantizamos que Mocha sepa detener la prueba de ese caso:

todos/index.test.js
...
done(err);
...

Proporcionamos el objeto err a done() de modo que Mocha pueda fallar en la prueba en caso de que se produzca un error.

Guarde y cierre index.test.js.

Ejecutaremos esta prueba con npm test como antes. En su consola se mostrará este resultado:

Output
... integrated test ✓ should be able to add and complete TODOs complete() ✓ should fail if there are no TODOs saveToFile() ✓ should save a single TODO 3 passing (15ms)

De esta manera, habrá probado su primera función asíncrona con Mocha usando callbacks. Sin embargo, en el momento en que se redactó este tutorial las promesas eran más prevalentes que los callbacks en el nuevo código Node.js, como se explica en nuestro artículo Cómo escribir código asíncrono en Node.js. A continuación, aprenderá a pobarlos con Mocha también.

Promesas

Una promesa es un objeto de JavaSript que, llegado el momento, mostrará un valor. Cuando una promesa se realiza correctamente, se resuelve. Cuando en ella se produce un error, se rechaza.

Modificaremos la función saveToFile() para que utilice promesas en vez de callbacks. Abra index.js:

  • nano index.js

Primero, debemos cambiar la forma en que se carga el módulo fs. En su archivo index.js, cambie la instrucción require() en la parte superior del archivo de modo que tenga este aspecto:

todos/index.js
...
const fs = require('fs').promises;
...

Acabamos de importar el módulo fs, que usa promesas en vez de callbacks. Ahora, debemos realizar algunos cambios en saveToFile() para que funcione con promesas.

En su editor de texto, aplique los siguientes cambios a la función saveToFile() para eliminar los callbacks:

todos/index.js
...
saveToFile() {
    let fileContents = 'Title,Completed\n';
    this.todos.forEach((todo) => {
        fileContents += `${todo.title},${todo.completed}\n`
    });

    return fs.writeFile('todos.csv', fileContents);
}
...

La primera diferencia es que nuestra función ya no acepta ningún argumento. Con promesas no necesitamos una función de callback. El segundo cambio hace referencia a la forma en que se escribe el archivo. Ahora, devolvemos el resultado de la promesa writeFile().

Guarde y cierre index.js.

Ahora adaptaremos nuestra prueba de modo que funcione con promesas. Abra index.test.js:

  • nano index.test.js

Cambie la prueba saveToFile() por lo siguiente:

todos/index.js
...
describe("saveToFile()", function() {
    it("should save a single TODO", function() {
        let todos = new Todos();
        todos.add("save a CSV");
        return todos.saveToFile().then(() => {
            assert.strictEqual(fs.existsSync('todos.csv'), true);
            let expectedFileContents = "Title,Completed\nsave a CSV,false\n";
            let content = fs.readFileSync("todos.csv").toString();
            assert.strictEqual(content, expectedFileContents);
        });
    });
});

El primer cambio que debemos realizar es eliminar el callback done() de sus argumentos. Si Mocha pasa el argumento done(), debe invocarse. De lo contrario, generará un error como este:

1) saveToFile()
       should save a single TODO:
     Error: Timeout of 2000ms exceeded. For async tests and hooks, ensure "done()" is called; if returning a Promise, ensure it resolves. (/home/ubuntu/todos/index.test.js)
      at listOnTimeout (internal/timers.js:536:17)
      at processTimers (internal/timers.js:480:7)

Al probar promesas, no incluya el callback done() en it().

Para probar nuestra promesa, debemos disponer nuestro código de aserción en la función then(). Observe que mostramos esta promesa en la prueba y que no tenemos una función catch() que capturar cuando se rechaza la Promise.

Mostramos la promesa para que cualquier error que aparezca en la función then() se extienda a la función it(). Si los errores no aparecen, Mocha no hará que el caso de prueba fracase. Al probar promesas, deberá usar return en la Promise que se esté probando. Si no es así, tendrá el riesgo de obtener una instancia de falso positivo.

También omitimos la clausula catch() porque Mocha puede detectar los casos en que se rechaza una promesa. Si se rechaza, automáticamente no superará la prueba.

Ahora que nuestra prueba está lista, guarde y cierre el archivo y luego ejecute Mocha con npm test, también para confirmar que el resultado sea satisfactorio:

Output
... integrated test ✓ should be able to add and complete TODOs complete() ✓ should fail if there are no TODOs saveToFile() ✓ should save a single TODO 3 passing (18ms)

Cambianis nuestro código y la prueba para usar promesas y ahora sabemos con seguridad que funciona. Sin embargo, los patrones asíncronos más recientes utilizan las palabras claves async y await a fin de que no tengamos que crear varias funciones then() para gestionar resultados satisfactorios. Vamos a ver cómo podemos hacer la prueba con async y await.

async/await

Las palabras claves async y await hacen que trabajar con promesas sea menos complicado. Una vez que definimos una función como asíncrona con la palabra clave async, podemos obtener cualquier resultado futuro en esa función con la palabra clave await. De esta forma, podemos usar promesas sin tener que usar las funciones then() o catch().

Podemos simplificar nuestra prueba saveToFile() basada en promesas con async y await. En su editor de texto, realice estas pequeñas ediciones en la prueba saveToFile() en index.test.js:

todos/index.test.js
...
describe("saveToFile()", function() {
    it("should save a single TODO", async function() {
        let todos = new Todos();
        todos.add("save a CSV");
        await todos.saveToFile();

        assert.strictEqual(fs.existsSync('todos.csv'), true);
        let expectedFileContents = "Title,Completed\nsave a CSV,false\n";
        let content = fs.readFileSync("todos.csv").toString();
        assert.strictEqual(content, expectedFileContents);
    });
});

El primer cambio es que la función usada por la función it() ahora tiene la palabra clave async cuando se define. Esto nos permite usar la palabra clave await dentro de su cuerpo.

El segundo cambio se encuentra cuando invocamos saveToFile(). La palabra clave await se usa antes de se invocación. Ahora, Node.js sabe que debe esperar hasta que se resuelva esta función antes de continuar con la prueba.

Nuestro código de función es más fácil de leer ahora que movimos el código que estaba en la función then() al cuerpo de la función it(). Ejecutar este código con npm test produce este resultado:

Output
... integrated test ✓ should be able to add and complete TODOs complete() ✓ should fail if there are no TODOs saveToFile() ✓ should save a single TODO 3 passing (30ms)

Ahora podemos probar las funciones asíncronas usando cualquiera de estos paradigmas asíncronos de forma apropiada.

Abarcamos un terreno amplio con las pruebas de código síncrono y asíncrono a través de Mocha. A continuación, profundizar en otras funcionalidades que ofrece Mocha para mejorar nuestra experiencia de prueba; en particular, la forma en que los enlaces pueden cambiar entornos de prueba.

Paso 5: Usar enlaces para mejorar casos de prueba

Los enlaces son una función útil de Mocha que nos permite configurar el entorno antes y después de una prueba. Normalmente, añadimos hooks en un bloque de funciones describe(), ya que contienen lógica de configuración y desglose específica para algunos casos de prueba.

Mocha ofrece cuatro enlaces que podemos usar en nuestras pruebas:

  • before: este enlace se ejecuta una vez antes de que se inicie la primera prueba.
  • beforeEach: este se ejecuta antes de cada caso de prueba.
  • after: este se ejecuta una vez después que se completa el último caso de prueba.
  • afterEach: este se ejecuta después de cada caso de prueba.

Cuando probamos una función o característica varias veces, los enlaces son útiles, ya que nos permiten separar el código de configuración de la prueba (como la creación del objeto todos) desde el código de aserción de esta.

Para ver el valor de los enlaces, añadiremos más pruebas a nuestro bloque de pruebas saveToFile().

Aunque confirmamos que podemos guardar nuestros elementos TODO en un archivo, solo guardamos un elemento. Además, el elemento no se marcó como completado. Añadiremos más pruebas para asegurarnos de que los diferentes aspectos de nuestro módulo funcionen.

Primero, añadiremos una segunda prueba para confirmar que nuestro archivo se guarde correctamente cuando hayamos completado un elemento TODO. Abra el archivo index.test.js en su editor de texto:

  • nano index.test.js

Cambie la última prueba para obtener lo siguiente:

todos/index.test.js
...
describe("saveToFile()", function () {
    it("should save a single TODO", async function () {
        let todos = new Todos();
        todos.add("save a CSV");
        await todos.saveToFile();

        assert.strictEqual(fs.existsSync('todos.csv'), true);
        let expectedFileContents = "Title,Completed\nsave a CSV,false\n";
        let content = fs.readFileSync("todos.csv").toString();
        assert.strictEqual(content, expectedFileContents);
    });

    it("should save a single TODO that's completed", async function () {
        let todos = new Todos();
        todos.add("save a CSV");
        todos.complete("save a CSV");
        await todos.saveToFile();

        assert.strictEqual(fs.existsSync('todos.csv'), true);
        let expectedFileContents = "Title,Completed\nsave a CSV,true\n";
        let content = fs.readFileSync("todos.csv").toString();
        assert.strictEqual(content, expectedFileContents);
    });
});

La prueba es similar a lo que teníamos antes. Las principales diferencias radican en que invocamos la función complete() antes de invocar saveToFile() y que ahora el valor de nuestros expectedFileContents es true en vez de false para el valor de la columna completed.

Guarde el archivo y ciérrelo.

Ejecutaremos nuestra nueva prueba y todas las demás con npm test:

  • npm test

Esto dará el siguiente resultado:

Output
... integrated test ✓ should be able to add and complete TODOs complete() ✓ should fail if there are no TODOs saveToFile() ✓ should save a single TODO ✓ should save a single TODO that's completed 4 passing (26ms)

Funciona como se espera. Sin embargo, esto se puede mejorar. Ambas tienen que crear una instancia de un objeto Todos al principio de la prueba. A medida que añadimos más casos de prueba, esto rápidamente se vuelve repetitivo y desperdicia mucha memoria. Además, cada vez que ejecutamos la prueba se crea un archivo. Alguien menos familiarizado con el módulo puede confundir esto con un resultado real. Estaría bien que limpiásemos nuestros archivos resultantes tras la prueba.

Realizaremos estas mejoras usando enlaces de pruebas. Usaremos el enlace beforeEach() para configurar nuestro artefacto de prueba de elementos TODO. Un artefacto de prueba es cualquier estado uniforme que se usa en una prueba. En nuestro caso, nuestro artefacto de prueba es un nuevo objeto todos que tiene un elemento TODO ya añadido. A continuación usaremos afterEach() para eliminar el archivo creado por la prueba.

En index.test.js, realice los siguientes cambios a su última prueba para saveToFile():

todos/index.test.js
...
describe("saveToFile()", function () {
    beforeEach(function () {
        this.todos = new Todos();
        this.todos.add("save a CSV");
    });

    afterEach(function () {
        if (fs.existsSync("todos.csv")) {
            fs.unlinkSync("todos.csv");
        }
    });

    it("should save a single TODO without error", async function () {
        await this.todos.saveToFile();

        assert.strictEqual(fs.existsSync("todos.csv"), true);
        let expectedFileContents = "Title,Completed\nsave a CSV,false\n";
        let content = fs.readFileSync("todos.csv").toString();
        assert.strictEqual(content, expectedFileContents);
    });

    it("should save a single TODO that's completed", async function () {
        this.todos.complete("save a CSV");
        await this.todos.saveToFile();

        assert.strictEqual(fs.existsSync('todos.csv'), true);
        let expectedFileContents = "Title,Completed\nsave a CSV,true\n";
        let content = fs.readFileSync("todos.csv").toString();
        assert.strictEqual(content, expectedFileContents);
    });
});

Desglosaremos todos los cambios que realizamos. Añadimos un bloque beforeEach() al bloque de pruebas:

todos/index.test.js
...
beforeEach(function () {
    this.todos = new Todos();
    this.todos.add("save a CSV");
});
...

Estas dos líneas de código crean un nuevo objeto Todos que estará disponible en cada una de nuestras pruebas. Con Mocha, el objeto this en beforeEach() hace referencia al mismo objeto this en it(). this es el mismo en todos los bloques de código dentro del bloque describe(). Para obtener más datos sobre this, consulte nuestro tutorial Información sobre This, Bind, Call y Apply en JavaScript.

Este potente intercambio de contexto es el motivo por el que podemos crear rápidamente artefactos que funcionarán para nuestras dos pruebas.

A continuación, limpiaremos nuestro archivo CSV en la función afterEach():

todos/index.test.js
...
afterEach(function () {
    if (fs.existsSync("todos.csv")) {
        fs.unlinkSync("todos.csv");
    }
});
...

Si nuestra prueba falló, es posible que no haya creado un archivo. Por eso verificamos si el archivo existe antes de usar la función unlinkSync() para eliminarlo.

Los cambios restantes cambian la referencia de todos, creados previamente en la función it(), a this.todos, que está disponible en el contexto de Mocha. También eliminamos las líneas que previamente crearon instancias de todos en los casos de prueba individuales.

Ahora, ejecutaremos este archivo para confimar que nuestras pruebas aún funcionen. Introduzca npm test en su terminal para obtener lo siguiente:

Output
... integrated test ✓ should be able to add and complete TODOs complete() ✓ should fail if there are no TODOs saveToFile() ✓ should save a single TODO without error ✓ should save a single TODO that's completed 4 passing (20ms)

Los resultados son los mismos y, como beneficio, redujimos ligeramente el tiempo de configuración para las nuevas pruebas de la función saveToFile() y encontramos una solución para el archivo CSV residual.

Conclusión

A lo largo de este tutorial , escribió un módulo de Node.js para administrar los elementos TODO y probó el código manualmente usando el REPL de Node.js. Luego, creó un archivo de prueba y usó el marco Mocha para ejecutar pruebas automatizadas. Con el módulo assert, pudo verificar que su código funciona. También probó funciones síncronas y asíncronas con Mocha. Finalmente, creó enlaces con Mocha que aportan legibilidad y una capacidad de mantenimiento muy superiores al escribir varios casos de prueba relacionados.

Ahora que cuenta con este conocimiento, anímese a escribir pruebas para nuevos módulos de Node.js que cree. ¿Puede pensar en las entradas y los resultados de su función y escribir su prueba antes que su código?

Si desea más información sobre el marco de prueba Mocha, consulte la documentación oficial de Mocha. Si desea seguir aprendiendo sobre Node.js, puede volver a la página de la serie Cómo desarrollar código en Node.js.

Creative Commons License