Testing básico y documentación en Node.js ES Modules
Cuando se trabaja con Node.js sin frameworks y con almacenamiento en archivos JSON, es habitual pensar que el testing no es necesario hasta tener una API completa. Pero justamente en este entorno tan simple es donde mejor se aprende a probar funciones, validar comportamientos y detectar errores sin depender de Express, routers o bases de datos.
Este documento explica cómo realizar pruebas automáticas básicas en proyectos Node.js puros, con ES Modules y persistencia en JSON, usando únicamente Vitest. El objetivo es aprender a probar funciones esenciales antes de introducir herramientas más avanzadas.
Enfoque práctico del testing en Node.js sin frameworks
En un proyecto básico sin Express ni SQLite, las capas a testear son otras. No existen rutas, middlewares ni controladores. Lo que vamos a probar es:
- funciones puras: validaciones, utilidades y lógica interna
- lectura y escritura de JSON
- funciones que procesan el cuerpo de una petición
- pequeñas simulaciones de peticiones HTTP usando objetos falsos
- módulos de seguridad: contraseñas, tokens simples, rate limiting
El propósito no es crear un sistema de testing complejo, sino aprender a comprobar que cada pieza hace lo que debe.
Configuración base con Vitest
Vitest es un framework ligero y rápido, ideal para Node.js puro y proyectos pequeños.
Instalación:
npm install -D vitest
En package.json:
{
"scripts": {
"test": "vitest run",
"test:watch": "vitest"
}
}
Archivo vitest.config.js:
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
globals: true,
environment: "node",
},
});
Con esto ya tienes listo tu entorno para testing.
Tests unitarios: funciones puras y utilidades
En Node.js sin frameworks, la mayoría de componentes importantes son funciones puras: validadores, generadores de tokens, módulos de hashing, o funciones que procesan JSON. Estas son perfectas para aprender testing porque no necesitan levantar ningún servidor.
Ejemplo: un validador simple
Archivo src/utils/validar.js:
export function validarEmail(email) {
return (
typeof email === "string" && email.includes("@") && email.length <= 100
);
}
export function validarPassword(pwd) {
return typeof pwd === "string" && pwd.length >= 6 && pwd.length <= 200;
}
Test: tests/validar.test.js:
import { validarEmail, validarPassword } from "../src/utils/validar.js";
describe("Validadores básicos", () => {
it("valida email correctamente", () => {
expect(validarEmail("test@example.com")).toBe(true);
expect(validarEmail("incorrecto")).toBe(false);
});
it("valida password correctamente", () => {
expect(validarPassword("123456")).toBe(true);
expect(validarPassword("123")).toBe(false);
});
});
Estos tests verifican que la lógica interna es correcta sin ningún servidor.
Testing de lectura y escritura de JSON
Si tu proyecto usa persistencia en JSON, también puedes probar que los módulos funcionan correctamente.
Ejemplo: src/utils/jsonFile.js:
import { readFile, writeFile } from "node:fs/promises";
export async function readData(path) {
try {
const content = await readFile(path, "utf8");
return JSON.parse(content);
} catch {
return [];
}
}
export async function writeData(path, data) {
const json = JSON.stringify(data, null, 2);
await writeFile(path, json, "utf8");
}
Test: tests/jsonFile.test.js:
import { readData, writeData } from "../src/utils/jsonFile.js";
import { rm } from "node:fs/promises";
const tempFile = "./tests/tmp/data.json";
describe("Lectura y escritura JSON", () => {
it("escribe y lee datos correctamente", async () => {
const datos = [{ id: 1, nombre: "Prueba" }];
await writeData(tempFile, datos);
const resultado = await readData(tempFile);
expect(resultado.length).toBe(1);
expect(resultado[0].nombre).toBe("Prueba");
});
it("devuelve lista vacía cuando el archivo no existe", async () => {
await rm(tempFile, { force: true });
const resultado = await readData(tempFile);
expect(Array.isArray(resultado)).toBe(true);
expect(resultado.length).toBe(0);
});
});
Este tipo de tests es fundamental para evitar corrupción de datos o errores de lectura.
Testing de funciones del servidor sin Express
Como no usamos Express, no podemos usar Supertest. Pero sí podemos simular peticiones HTTP creando objetos falsos que imiten req y res.
Ejemplo: testear la función readJsonBody
Archivo src/utils/readBody.js:
export function readJsonBody(req) {
return new Promise((resolve, reject) => {
let data = "";
req.on("data", (chunk) => {
data += chunk;
if (data.length > 1_000_000) {
reject(new Error("Body demasiado grande"));
req.destroy();
}
});
req.on("end", () => {
try {
const json = JSON.parse(data || "{}");
resolve(json);
} catch {
reject(new Error("JSON inválido"));
}
});
req.on("error", reject);
});
}
Test: tests/readBody.test.js:
import { readJsonBody } from "../src/utils/readBody.js";
import { PassThrough } from "node:stream";
function crearRequestSimulado(payload) {
const req = new PassThrough();
process.nextTick(() => {
req.write(payload);
req.end();
});
return req;
}
describe("Lector JSON seguro", () => {
it("parsea JSON válido", async () => {
const req = crearRequestSimulado('{"nombre":"test"}');
const data = await readJsonBody(req);
expect(data.nombre).toBe("test");
});
it("rechaza JSON inválido", async () => {
const req = crearRequestSimulado("{incompleto");
await expect(readJsonBody(req)).rejects.toThrow("JSON inválido");
});
});
Esta técnica permite probar funciones del servidor sin servidor.
Testing de seguridad: hashing, tokens y rate limiting
El testing de funciones de seguridad es muy instructivo.
Ejemplo: src/utils/seguridad.js:
import crypto from "node:crypto";
export function hashPassword(password) {
const salt = crypto.randomBytes(16).toString("hex");
const hash = crypto.scryptSync(password, salt, 64).toString("hex");
return `${salt}:${hash}`;
}
export function verificarPassword(password, almacenado) {
const [salt, hash] = almacenado.split(":");
const test = crypto.scryptSync(password, salt, 64);
return crypto.timingSafeEqual(Buffer.from(hash, "hex"), test);
}
Test:
import { hashPassword, verificarPassword } from "../src/utils/seguridad.js";
describe("Hashing de contraseñas", () => {
it("genera hashes distintos incluso con la misma contraseña", () => {
const h1 = hashPassword("abc123");
const h2 = hashPassword("abc123");
expect(h1).not.toBe(h2);
});
it("verifica contraseñas correctamente", () => {
const hash = hashPassword("secreto");
expect(verificarPassword("secreto", hash)).toBe(true);
expect(verificarPassword("otro", hash)).toBe(false);
});
});
Esto te prepara para autenticación real más adelante.
Simulación básica de un endpoint completo sin Express
Puedes testear módulos que representen un “endpoint” manual, usando funciones que reciben datos y devuelven respuestas.
Ejemplo: src/endpoints/crearUsuario.js:
import { validarEmail, validarPassword } from "../utils/validar.js";
export function crearUsuario(datos) {
if (!validarEmail(datos.email)) return { ok: false, error: "Email inválido" };
if (!validarPassword(datos.password))
return { ok: false, error: "Password inválido" };
return { ok: true };
}
Test: tests/crearUsuario.test.js:
import { crearUsuario } from "../src/endpoints/crearUsuario.js";
describe("Endpoint manual: crear usuario", () => {
it("valida email correctamente", () => {
const res = crearUsuario({ email: "malo", password: "123456" });
expect(res.ok).toBe(false);
});
it("valida contraseñas", () => {
const res = crearUsuario({ email: "test@mail.com", password: "123" });
expect(res.ok).toBe(false);
});
it("acepta datos correctos", () => {
const res = crearUsuario({ email: "a@a.com", password: "abcdef" });
expect(res.ok).toBe(true);
});
});
Esto imita un endpoint pero sin necesidad de un servidor real.
Recomendaciones finales
- Mantén los tests simples y enfocados en funciones concretas.
- Usa objetos falsos (mock objects) para simular peticiones.
- Evita probar flujos complejos hasta usar Express o un framework real.
- Añade pruebas conforme crezca tu proyecto, no al final.
Este enfoque te prepara perfectamente para testing más avanzado cuando pases a Express, SQLite o APIs completas.
Módulo adicional: uso de mocking en Node.js puro con ES Modules
En proyectos sin frameworks, el mocking es especialmente útil porque muchas funciones dependen de módulos nativos (fs, crypto, stream, etc.) o de tu propia estructura interna. Mockear te permite aislar partes del código para probarlas sin efectos secundarios.
Aquí aprenderás:
- Cómo mockear módulos propios
- Cómo mockear módulos de Node
- Cómo simular respuestas sin un servidor real
- Cómo usar mocking para pruebas más fiables y rápidas
Vitest facilita todo esto con su API vi.mock.
Mocking de módulos propios
Supón que tu código depende de un módulo que escribe en disco, pero no quieres escribir realmente durante el test.
Archivo real: src/utils/jsonFile.js:
export async function writeData(path, data) {
console.log("Escribiendo en disco...");
}
Test con mock:
import { writeData } from "../src/utils/jsonFile.js";
import { vi } from "vitest";
vi.mock("../src/utils/jsonFile.js", () => ({
writeData: vi.fn().mockResolvedValue(undefined),
}));
describe("Mock de módulo propio", () => {
it("llama a writeData sin escribir en disco", async () => {
await writeData("ruta.json", []);
expect(writeData).toHaveBeenCalled();
});
});
Ventajas:
- No crea archivos reales
- No necesitas limpiar nada después
- Puedes controlar exactamente qué devuelve
Mocking de módulos nativos de Node (por ejemplo: fs/promises)
Esto es especialmente útil si quieres probar funciones que leen JSON desde disco sin depender del archivo real.
Mock de fs/promises:
vi.mock("node:fs/promises", () => ({
readFile: vi.fn().mockResolvedValue('[{"id":1}]'),
writeFile: vi.fn().mockResolvedValue(undefined),
}));
Ejemplo completo para testear readData:
import { readData } from "../src/utils/jsonFile.js";
import { vi } from "vitest";
vi.mock("node:fs/promises", () => ({
readFile: vi.fn().mockResolvedValue('[{"id":1}]'),
writeFile: vi.fn(),
}));
describe("Mock de fs/promises", () => {
it("lee datos simulados en lugar de leer el archivo real", async () => {
const datos = await readData("ruta.json");
expect(datos[0].id).toBe(1);
});
});
Con esto puedes probar tu lógica de lectura sin acceder al disco.
Mocking de streams para simular req y res
Para testear funciones como readJsonBody o cualquier otra que use streams, puedes crear objetos simulados:
import { PassThrough } from "node:stream";
function mockRequest(payload) {
const req = new PassThrough();
process.nextTick(() => {
req.write(payload);
req.end();
});
return req;
}
O puedes mockear el evento req.on directamente:
const req = {
on: vi.fn((event, handler) => {
if (event === "data") handler('{"ok":true}');
if (event === "end") handler();
}),
destroy: vi.fn(),
};
Esto permite probar funciones relacionadas con peticiones sin levantar un servidor.
Mocking de crypto
Útil si quieres pruebas deterministas:
vi.mock("node:crypto", () => ({
randomBytes: () => Buffer.from("abcd1234abcd1234abcd1234abcd1234"),
scryptSync: () => Buffer.from("hash-simulado"),
timingSafeEqual: () => true,
}));
Beneficios:
- Tus tests no dependen de valores aleatorios
- Puedes comprobar flujo y control, no valores concretos