🇺🇦 Слава Україні! Consulta cómo puedes ayudar a Ucrania desde España u otros países en supportukrainenow.org.

Cursos y talleres

Cómo usar node:test en NodeJS 18 y 19

• Duración: 9:41 • #testing #javascript #nodejs

¿Sabías que tal vez no necesites Vitest? ¿O Jest? NodeJS trae desde la versión 18 un runner que se ejecuta con `node --test`, y que permite ejecutar suites y colecciones de tests que declaremos en nuestros archivos si utilizamos los módulos node:test y node:assert.

Desde la versión 18 de NodeJS, el runtime incorpora su propio framework de testing. Este framework probablemente no esté en un punto de madurez tal que vaya a dejar obsoletas herramientas como Vitest, Jest o Mocha, pero permite prototipar rápidamente proyectos al no requerir configurar un sistema de pruebas unitarias, pudiendo usar el que viene integrado por el propio runtime.

Cómo invoco el test runner de NodeJS 18

Para poder utilizar este runtime, todo lo que tienes que hacer es invocar node como node --test. Esto configurará NodeJS en modo test runner, y lo que hará en este modo es localizar archivos de test compatibles dentro de la carpeta en la que está, e invocarlos. Existen algunas normas que tienes que tener en cuenta cuando hagas esto, no obstante. El test runner buscará todos los archivos compatibles en el directorio que esté y en subdirectorios, siguiendo una serie de reglas de inferencia:

  • Si se dice explícitamente el archivo a ejecutar, como node --test testing.js, entonces se cargarán las suite de tests que haya dentro de ese archivo.
  • Se ignora la carpeta nodemodules salvo que se diga lo contrario (por ejemplo, `node --test nodemodules/test.js` sí que cargaría esa suite de test).
  • Si encuentra una carpeta o subcarpeta llamada test/, entonces ejecuta todos los archivos de tipo .js, .mjs y .cjs que encuentre dentro.
  • Si encuentra archivos que terminen por test y que tengan la extensión .js, .mjs o .cjs, como calctest.js, usertest.mjs o invoketest.cjs, se cargarán.

Si no eres capaz de ejecutar tu test usando el test runner de Node 18, comprueba que no estés entorpeciendo que encuentre el archivo.

El formato del test runner: TAP

Cuando se ejecuta este runner, correrá los tests contenidos en las suites que se haya cargado, y reportará finalmente su estado por salida. Tiene un formato un poco extraño, que se basa en el formato TAP (Test Anything Protocol). Tiene la ventaja de que es un formato universal, así que existen plugins y otras extensiones capaces de interpretar esta salida para poder mostrarlo de forma bonita, pero tiene la desventaja de que sin herramientas es un poco complicado de digerir a veces.

Por ejemplo, esta sería una salida del test runner usando el formato TAP.

TAP version 13
1..0
# tests 0
# pass 0
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 10.010375

Se incluyen cosas como el número de tests que se han ejecutado, cuántos de estos tests han salido bien, cuántos han salido mal, cuántos han sido saltados y cuántos han sido cancelados. Generalmente los tests que salen bien no reportarán mucha información, pero los tests que salen mal sí que reportarán información con trazas sobre por qué ha fallado y donde.

Cómo escribir tests con el runner de Node

Para escribir estos tests, dependerás fundamentalmente de dos paquetes, node:test y node:assert. Empecemos con el primero.

node:test permite importar las funciones que delimitan tests. Hay dos formas de escribir un test. La primera será importar tal cual todo el módulo como una función llamada test y escribir los tests del siguiente modo:

import test from "node:test"
// const test = require("node:test")

test("ejemplo de un test", () => {
  // aquí va el contenido de mi test
});

test("ejemplo de otro test", () => {
  // aquí va el contenido de mi otro test
});

En este caso, test será una función de dos parámetros. La primera función es un string con el nombre que le queremos dar al test, y la segunda función es un callback con lo que queremos que haga el test. Esencialmente en este modo, lo que hará NodeJS será procesar cada llamada a la función test, ejecutando cada callback proporcionado como parámetro. El primer parámetro, con el nombre del test, será mostrado en reportes para identificarlo y así saber cuál es el test que falla.

Sin embargo, si queremos escribir tests de maneras parecidas a como lo hace la gente que aplica todo el tiempo TDD y BDD, también podemos importar las funciones describe e it de este paquete:

import { describe, it } from "node:test"
// const { describe, it } = require("node:test")

describe("contenedor de tests", () => {
  it("test 1", () => {
    // Aquí va el contenido del test
  });
  it("test 2", () => {
    // Aquí va el contenido de otro test
  });
});

Esta forma de trabajar puede que te suene de otros frameworks como Jest, porque de hecho es el mismo funcionamiento. NodeJS ejecutará cada bloque describe, y para cada callback, anidará los tests que hay en las llamadas a it. Este anidamiento se puede repetir metiendo una llamada a describe dentro de otro describe. La idea de este principio es repartir jerárquicamente nuestros tests para organizar mejor el archivo, así como compartir información en los casos, tal vez mediante el uso de funciones como before o after (que también pueden ser importadas desde el mismo paquete node:test siguiendo el mismo sistema).

Una vez hayamos decidido cómo escribir nuestros tests, tenemos que ponernos de acuerdo sobre qué meter dentro. Para ello, como en cualquier test, tendremos que escribir asertos. Estos asertos los podemos importar desde el paquete node:assert, y lo recomendable es importar el paquete completo:

import assert from "node:assert/strict"
// const assert = require("node:assert/strict")

(Nota: si bien podemos importar node:assert en vez de node:assert/strict, es recomendable evitarlo, y usar en su lugar node:assert/strict, para evitar algunos errores especiales que pueden ocurrir con el node:assert normal).

Dentro de este módulo hay funciones de asertos que podemos usar para comprobar si dos cosas son iguales, del mismo modo que ocurre con otros frameworks de test. Algunas de estas son equals, que nos permite comprobar si los dos valores que le pasamos como parámetros son iguales. Por ejemplo:

test("assert that count() returns 4", () => {
  const expected = 4;
  const actual = count();
  assert.equals(expected, actual);
});

En este caso, si expected y actual tienen el mismo valor, el test pasará. Más bien, no generará ningún tipo de error y tendrá una salida de test mucho más discreta. Sin embargo, si los dos valores fuesen diferentes, equals provocaría un error que detendría la ejecución de este test y finalmente veríamos la traza de error en la consola.

Otras funciones útiles son deepEquals, que comprueba si los dos parámetros que le pasamos son exactamente iguales incluyendo cualquier propiedad anidada dentro, ideal para ser usada con objetos JavaScript o mapas. También está ok, que simplemente comprueba si el parámetro que le pasemos sea verdadero.

test("este test va a fallar", () => {
  ok(2 + 2 === 5);
});

test("este test ya no va a fallar", () => {
  ok(2 + 2 == 4);
});
Desplegar transcripción del episodio

Node.js incorpora hoy en día un test runner que te permite correrte rápidamente sin tener que instalar ningún tipo de dependencia. Eso no significa que vitest y que chai y que mocha y que Jest hayan dejado de ser útiles, pero, sin embargo, puede hacerte más fácil el prototipo de proyectos nuevos al no tener que instalar demasiadas dependencias y tener que configurarlas. Simplemente escribes tus archivos de test dentro del proyecto, ejecutas node --test y ya lo tienes listo. En este video te contaré cómo puedes utilizar las APIs node:test y node:assert para poder escribir tus tests y para no hacer demasiado largo este video dejaré para una segunda parte mostrar algunos aspectos más avanzados que nos proporciona el runner de Node.js. Pero antes de empezar, aprecia esta salida. Es un poco caótica. Esto utiliza el formato TAP. Cada vez que escribamos un test va a ser más larga, y es un poco complicado de comprender a veces qué es lo que está ocurriendo. Esperemos que en versiones futuras de Node.js la cosa mejora y a lo mejor tenga hasta colorines. Sin embargo, por suerte, lo más importante se lo reservan para el final. Así que, incluso en el caso de que Node --test te suelte toda una traza gigante de líneas de texto, por lo menos lo importante lo vas a encontrar al final. Este test nos va a decir cuántos test han ejecutado, en este caso cero porque no hay test hechos. Este pass nos va a decir cuántos de esos tests han salido bien, en este caso cero porque no hay test. Y fail nos va a decir cuántos test han salido mal, en este caso cero porque sigue sin haber test. Y luego también tenemos adicionalmente test que hayan sido saltados o que no estén terminados o lo que sea. Pero esta salida es un poco caótica. Sin embargo, la ventaja es que TAP es un formato estándar, lo que significa que existen herramientas que pueden interpretar la salida de un comando TAP y mostrártelo de forma bonita. Incluso es posible que las puedas instalar en tu propio editor de textos o IDE. Otra de las cosas importantes del runner de Node.js es que va a hacer un descubrimiento automático de los test de nuestro proyecto. Es decir, nosotros simplemente escribimos node --test y va a buscar dentro de nuestro proyecto archivos de test. Para ello le tenemos que ayudar un poco, obviamente. Y como todos son archivos de JavaScript, tenemos que tener una nomenclatura clara a la hora de escribir nuestros archivos de test para que el runner los pueda encontrar. Si no siempre le podemos decir node --test y la ruta al test JS que queramos testear o la ruta al archivo .js que queramos testear, y gracias a ello, pues va a ser capaz de encontrar correctamente el test. Pero por defecto, Node test va a buscar en todas partes. Así que es importante tener una nomenclatura clara para no confundirle. Por ejemplo, si en nuestro proyecto creamos una carpeta llamada test, todo lo que cuelgue dentro de esta carpeta va a ser considerado un test. Así que si yo pongo aquí simple.js, puedo escribir mi test y en principio lo va a pillar correctamente. Pero hay gente a la que no le gusta tener los test separados del código y que prefiere tenerlo en un conjunto. Es decir, tener su calculadora.js y tener su calculadoratest.js. Pues no hay problema, mientras tú lo llames _test al final del archivo, va a ser un archivo de test que va a pillar correctamente. En este caso, fíjate que si lo pongo como calctest.js ya encuentra calctest.js. Este sistema es apreciado por algunas personas porque gracias a él puedes tener emparejado tu código y tu test y así por lo menos si haces un cambio no se te olvida actualizar el test. En mi caso, para lo que es esta demo, me voy a quedar únicamente con un archivo de test para que podamos probar qué tenemos que meter aquí dentro. Y buena pregunta, ¿qué tenemos que meter aquí dentro? Bueno, lo primero que tendremos que hacer es importar las bibliotecas de testeo. Y para ello lo que haremos será importar dos módulos, node:test, que sirve para importar como tal las funciones propias de los test y node:assert. Y apreciaréis que hay dos bibliotecas node:assert, la assert y la assert/strict. Me consejo es que importeas la strict, os dará menos problemas si tenéis que comparar cosas que lo mejor parecen iguales pero no lo son. También apreciaréis que bien día se utiliza el prefijo node: para señalizar aquellos paquetes que forman parte de la biblioteca estándar de Node.js, así por lo menos no entren en conflicto con otros que tengamos en nuestro nodemodules. Pero claro, así es como no se importan las cosas, nosotros tendremos que poner el import tradicional, importando por ejemplo el módulo test como test e importando el módulo assert/strict como assert. Y por cierto, por si os lo estáis preguntando, sí, también podéis importar estos paquetes utilizando require si tenéis un proyecto de CommonJs que todavía está usando require y module.exports. Cuando importamos node:test de este modo como import test, lo que hace es importar una función que podemos utilizar para fabricar test. Esta función tendrá dos parámetros, el primero será el nombre que queremos que tenga el test, por ejemplo un test, y el segundo será una función anónima que definirá un callback que se tiene que ejecutar cuando se evalúe este test y lo que hacemos aquí dentro es meter tal cual el código de nuestro test y dejar que lo ejecute. En este caso lo voy a dejar en blanco para que veáis que si yo lo llamo múltiples veces, cada llamada test me genera un test separado. Y ahora cuando ponga node --test, vamos a ver que en la salida nos aparece que se ha ejecutado un test y dos test y nos muestra además con uno que hay que los test han salido bien porque evidentemente están vacíos. Como ya puedo escuchar al fondo las quejas de la gente que prefiere utilizar describe, simplemente decir que si en vez de poner import test lo ponemos con llaves, dentro podemos extraer las funciones describe e it y utilizarlo por medio de bloques como se hacen otras bibliotecas de testeo. De este modo podríamos utilizar bloques de describe para agrupar test que pueden estar escritos utilizando it. La sintaxis es exactamente la misma, comentario con el nombre del test y luego una función anónima que sirva para proporcionar el contenido de cada uno de esos test. Dicho ese de paso, en Node 18 y Node 19 existe un bus que esperemos que se ha corregido en el futuro que es que no puedes utilizar los acentos porque eso confunde muchísimo el runner. Así que me vais a perdonar mi ortografía pero tengo que escribirlo así si quiero poder hablar en español y que se me entienda. Y en este caso la salida es mucho más caótica porque ya empezamos a anidar cosas pero es la consecuencia de estar utilizando describe. Tenemos mi colección, mi colección 1 caso 1, mi colección, mi colección 1 caso 2, mi colección, colección 2 caso 1 y mi colección, colección 2 caso 2. Todos con su OK correspondiente pero es un poco complicado en algunas ocasiones saber cuántos test se han ejecutado porque no vemos el 4 debido a que cada colección se trata como si fuese un bloque separado. Esto es algo que tendrás que tener en cuenta y aunque te puedes apoyar de plugins que hacen más fácil entender la salida, a lo mejor eso implica que tienes que escribir tu test de una forma más simple para que los puedas comprender o simplemente retirarte y utilizar una biblioteca de testeo como se ha hecho toda la vida. Vale, pero ¿cómo utilizaría realmente los test para hacer cosas útiles? Voy a sustituir estos describes y voy a dejar solamente test para que así sea más fácil de comprender, ¿ok? Y por supuesto voy a cambiarlo por test. Vamos a utilizar assert para poder comprobar en este caso que la salida de la función sumar da lo que tiene que dar. Voy a meter intencionadamente un bug dentro de mi función y voy a escribir tal cual un assert que me compruebe que sumar de 2 más 3 de 5, ¿vale? ¿Cómo lo haría? Pues vamos a ver si yo saco mi resultado como sumar de 2, 3. A lo mejor me gustaría comprobar que lo esperado sea 5. ¿Cómo escribo un test que me compruebe que sumar 2, 3 da 5? Pues como haría con cualquier otra biblioteca de testeo. En este caso yo, como he importado assert, pongo assert. y utilizo alguna de las funciones, en este caso equal, que es un assert que comprueba que los dos parámetros que le proporcionemos sean iguales. Y en caso de que no lo sean, generará una señal que provocará que el test falle y nos muestra el error en la consola. ¿Es posible ponerle llaves al assert e importar únicamente la función igual? Sí, pero mi consejo es que no lo hagáis y que importéis tal cual todo el módulo assert. Porque de ese modo es mucho más fácil de comprender y no tenemos que preguntarnos si este equal es de una variable que ya hemos puesto o si es el equal que viene de la biblioteca no de assert. Y en este caso lo que voy a hacer es comprobar que result se corresponde con esperado. En cada suede de test, el orden de los parámetros es diferente pero al final da igual. Comprueba lo que tiene con la izquierda y con lo de la derecha y falla si no son iguales. Y en este caso, como no son iguales porque he metido un bug, me lo va a marcar como error y los errores tienen esta forma tan caótica. Nos muestra el mensaje de error por el cual no ha salido bien. En este caso, se esperaba que el valor fuese igual pero no lo ha sido porque menos uno es diferente que cinco. Y también nos muestra el stack por si queremos encontrarlo fácilmente dentro del archivo. Además, nos ha pasado el fail a 1 y también el comando sale con error, por eso me sale en mi terminal de color rojo. La manera que tengo de corregir esto sería arreglando el código que falla y si cambio al menos por un más eso provoca que el test ya pase correctamente. Ahora la salida, como veis, es mucho más corta y nos dice que las cosas suman es un test que pasa correctamente. Es decir, no nos vamos a enterar cuando las cosas salen bien pero si nos vamos a enterar cuando las cosas salen mal. Ya que cuando las cosas salen bien, la salida va a ser mucho más minimalista. Por supuesto, el módulo assert es un poco más grande y tiene más assertos. Por ejemplo, ok, me gusta mucho para situaciones en las cuales simplemente quiero comprobar que algo es verdadero. Si yo le pongo ok de una expresión como por ejemplo 2 más 2 es igual a 4 o es triple igual a 4, este ok va a fallar salvo que esto sea verdadero. En este caso no debería fallar porque se supone que 2 más 2 son 4, pero aprecia como cuando cambio lo que hay en la expresión para que deje de ser verdadero, automáticamente el test falla. Y no tenemos que estar poniendo equal true o equal false. Con deepEqual podemos comprobar que dos cosas son iguales metiéndonos más para dentro. Por ejemplo, si tenemos arrays o objetos, podemos directamente comprobar que los objetos son completamente iguales y no simplemente que sus referencias cuadren. Muchos assertos son simplemente una versión negada, por ejemplo, de equal tenemos notEqual y de deepEqual tenemos notDeepEqual. Y también tenemos algunos assertos un poquito especiales como por ejemplo throws o rejects que sirven para comprobar errores. Y que os los quiero enseñar en un ejemplo que voy a subir por separado. Así no me queda un vídeo largo y no me penaliza tanto YouTube por enrollarme. Ya sé que es molesto, pero la gente es adicta a los vídeos de dos minutos. Así que nada, me gustaría despedirme con una pregunta. Y es, ¿creéis que esto puede ser útil o de momento preferís seguir utilizando vitest y similares? Contádmelo en los comentarios que eso también ayuda a que el vídeo despegue. Ya sé que estoy muy pesado con lo de que el vídeo despegue, pero es que si este vídeo te ha ayudado a ti, lo mejor que puedes hacer es ayudar a que le siga siendo útil a más personas. Así que dale me gusta si te ha gustado. Nos vemos en próximos vídeos. Un saludo y hasta luego.