Existen muchas herramientas para crear tests unitarios en JS. Nosotros usaremos Vitest que es una adaptación para Vite de la librería Jest.
Podemos instalarlo con npm en el proyecto donde lo vayamos a usar:
npm install -D vitest
o podemos instalarlo globalmente para usarlo en cualquier proyecto (npm i -g vitest
). Si lo hacemos así deberemos crear un fichero package.json en nuestro directorio de trabajo con el código:
{
"scripts": {
"test": "vitest"
}
}
Hemos implementado una función de suma en el fichero sum.js:
function sum(a, b) {
return a + b;
}
export sum;
NOTA: si no estamos usando _Vite__ podemos exportar la función con module.exports = sum;
Para probarla crearemos el fichero sum.test.js con el siguiente contenido:
import { sum } from "./sum.js";
describe("sum", () => {
test("sumar 1 + 2 es igual a 3", () => {
expect(sum(1, 2)).toBe(3);
});
test("sumar 1 + -2 es igual a -1", () => {
expect(sum(1, -2)).toBe(-1);
});
});
NOTA: En lugar de test
podemos usar it
que es equivalente.
Para pasar el test ejecutaremos en la terminal:
npm run test
La estructura de un fichero de test es:
describe
test
. Cada test debe ser independiente de los demás y no depender de su orden de ejecución.Podemos ver más opciones en la documentación oficial.
Cada test utiliza la función expect
que recibe un valor y utiliza un matcher para compararlo con lo esperado. La sintaxis básica es como hemos visto en el ejemplo:
expects(data).toBe(value);
NOTA: En lugar de expects
podemos usar assert
que es equivalente.
Algunos marcadores que podemos usar son:
A todos ellos se les puede anteponer .not. para negarlos (ej. expect(sum(2, 2)).not.toBe(3)
)
Para comparar números:
Y podemos ver si un string cumple una expresión regular:
También podemos comprobar si un array contiene un elemento
o si una variable es instancia de un tipo concreto
Podemos testear una función que devuelva una promesa u otras funciones asíncronas. Si tenemos la función fetchData que devuelve una promesa que al resolverse devuelve el texto ‘peanut butter’ la testearemos con el código:
describe("fetchData", () => {
test("fecthData must return a promise", () => {
let data = fetchData();
expect(data).toBeInstanceOf(Promise);
});
test("the data is peanut butter", async () => {
let data = await fetchData();
expect(data).toBe("peanut butter");
});
});
Podemos simular el comportamiento variables, funciones o el prpio navegador con mocks y así no alterar los valores reales de nuestros datos.
Para simular funciones vitest incorpora la utilidad vi. Esta utilidad tiene 2 métodos que son fn()
y spyOn()
que nos permiten hacer mocks de funciones:
import { vi } from "vitest";
const myFunctionMock = vi.fn(() => "mocked value");
myFunctionMock();
expect(myFunctionMock).toHaveBeenCalled();
myFunctionMock.mockReturnValue("mocked value");
const myFunctionSpy = vi.spyOn(myFunction);
myFunctionSpy.mockImplementation(() => "mocked value");
Para simular la red vitest recomienda usar msw que es una librería que nos permite simular peticiones a la red. Para instalarla:
npm install -D msw
y para usarla:
import { http, HttpResponse } from "msw";
import { setupServer } from "msw/node";
const mockUsers = [
{ id: 1, name: "John Doe" },
{ id: 2, name: "Jane Doe" },
];
const restHandlers = [
http.get("http://localhost:3000/users", () => {
return HttpResponse.json(mockUsers);
}),
http.get("http://localhost:3000/users/1", () => {
return HttpResponse.json({ id: 1, name: "John Doe" });
}),
http.get("http://localhost:3000/users/999", (req, res, ctx) => {
return res(ctx.status(404));
}),
http.post("http://localhost:3000/users", async ({ request }) => {
const body = await request.json();
return HttpResponse.json({ id: 3, ...body });
}),
http.delete("http://localhost:3000/users/:id", (req, res, ctx) => {
const id = parseInt(req.params.id);
const existentIds = mockUsers.map((user) => user.id);
return existentIds.includes(id)
? HttpResponse.json({})
: HttpResponse.notFound();
}),
http.put("http://localhost:3000/users/:id", async ({ request }) => {
const id = parseInt(req.params.id);
const existentIds = mockUsers.map((user) => user.id);
if (!existentIds.includes(id)) {
return HttpResponse.notFound();
}
const body = await request.json();
return HttpResponse.json(body);
}),
http.put("http://localhost:3000/users/100", (req, res, ctx) => {
return res(ctx.status(404));
}),
];
const server = setupServer(
rest.get("/greeting", (req, res, ctx) => {
return res(ctx.json({ greeting: "hello there" }));
})
);
beforeAll(() => server.listen());
afterAll(() => server.close());
test("fetches greeting", async () => {
const response = await fetch("/greeting");
const data = await response.json();
expect(data.greeting).toBe("hello there");
});
Para simular el navegador vitest recomienda usar jsdom que es una librería que nos permite simular el navegador. Para instalarla:
npm install -D jsdom
Y agregamos el siguiente comentario al principio del archivo de pruebas:
/**
* @vitest-environment jsdom
*/
Para usarla:
import { JSDOM } from 'jsdom';
const dom = new JSDOM('<!DOCTYPE html><p>Hello world</p>');
cont myParagraph = dom.window.document.querySelector('p')
console.log(myParagraph.textContent); // "Hello world"
test('the paragraph contains "Hello world"', () => {
expect(myParagraph.textContent).toBe('Hello world');
});
Podemos ver la cobertura de nuestro código con el comando:
npm run test -- --coverage
Esto nos mostrará un resumen de la cobertura de nuestro código y nos creará una carpeta coverage con un informe detallado.
El desarrollo guiado por test es una técnica de programación que consiste en escribir primero los tests y después el código que los pasa. De esta forma nos aseguramos de que el código que escribimos es el mínimo necesario para pasar los tests y no más.
Esta técnica se basa en el ciclo Red-Green-Refactor que consiste en:
Por ejemplo, si queremos escribir una función que devuelva el mayor de 2 números escribiremos un test que falle:
test("max(1, 2) should return 2", () => {
expect(max(1, 2)).toBe(2);
});
y después escribiremos el código necesario para que pase:
function max(a, b) {
return a > b ? a : b;
}