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);
});