===== Test unitarios =====
Se implementan para testear la unidad mínima de código, que normalmente serán funciones o métodos, de forma totalmente aislada del resto de la aplicación.
==== Clases de utilidad ====
const expect = require('chai').expect;
const { getDaysFromNow, getDays} = require('../../dateUtils');
beforeAll(() => {
jest.useFakeTimers();
jest.setSystemTime(new Date('2025-02-13'));
});
afterAll(() => {
jest.useRealTimers();
});
describe('dateUtils', () => {
// TODO Añadir casos de prueba para la función getYearsFromNow
it('getDaysFromNow', () => {
let days = getDaysFromNow(new Date('2025-02-12'));
expect(days).equal(1);
days = getDaysFromNow(new Date('2025-01-13'));
expect(days).equal(31);
});
it('getDays', () => {
let days = getDays(new Date('2025-01-12'), new Date('2025-01-15'));
expect(days).equal(3);
days = getDays(new Date('2025-01-01'), new Date('2024-01-01'));
expect(days).equal(366);
days = getDays(new Date('2024-01-01'), new Date('2024-01-01'));
expect(days).equal(0);
});
});
==== Capa controller ====
const httpMocks = require('node-mocks-http');
const { describe, it, expect, afterEach } = require('@jest/globals');
jest.mock('../../service/cities');
const cityController = require('../../controller/cities');
const cityService = require('../../service/cities');
const mockedFindCities = jest.spyOn(cityService, 'findCities');
const mockedRegisterCity = jest.spyOn(cityService, "registerCity");
const { mockCityArray, mockCityToPost, mockCityResponse, mockCityToRegister } = require('./mocks/cities');
afterEach(() => {
jest.clearAllMocks();
});
describe('cities', () => {
it('GET /cities should get a city list', async () => {
const response = httpMocks.createResponse();
const request = httpMocks.createRequest();
request.app = {};
request.app.conf = {};
request.path = '/cities';
const mockedCityList = jest.fn(async () => {
return mockCityArray;
});
mockedFindCities.mockImplementation(mockedCityList);
await cityController.getCities(request, response);
expect(mockedFindCities).toHaveBeenCalledTimes(1);
expect(response.statusCode).toEqual(200);
expect(response._isEndCalled()).toBeTruthy();
expect(response._getJSONData().length).toEqual(5);
});
it('POST /cities should register a new city', async () => {
const response = httpMocks.createResponse();
const request = httpMocks.createRequest();
request.app = {};
request.app.conf = {};
request.path = '/cities';
request.body = mockCityToRegister;
const mockedRegisterCityResponse = jest.fn(async () => {
return mockCityResponse;
});
mockedRegisterCity.mockImplementation(mockedRegisterCityResponse);
await cityController.postCity(request, response);
expect(mockedRegisterCity).toHaveBeenCalledTimes(1);
expect(response.statusCode).toEqual(201);
expect(response._isEndCalled()).toBeTruthy();
expect(response._getJSONData().id).toEqual(1);
expect(response._getJSONData().name).toEqual('Zaragoza');
expect(response._getJSONData().population).toEqual(700000);
expect(response._getJSONData().altitude).toEqual(200);
expect(response._getJSONData().age).toEqual(5);
expect(response._getJSONData().area).toEqual(734734);
expect(response._getJSONData().density).toEqual(2373);
expect(response._getJSONData().foundationDate).toEqual('2020-12-01');
});
});
. . .
"scripts": {
"unit-test": "jest src/test/unit",
. . .
. . .
santi@zenbook:$ npm install -g jest
===== Test de integración =====
En este caso escribimos tests que permitan probar varios componentes juntos para verificar que trabajan bien cuando interactuan entre si.
* Caso de uso: **Ok (200 OK)**
const chai = require('chai');
const expect = chai.expect;
const chaiHttp = require('chai-http');
const app = require('../../app').app;
chai.use(chaiHttp);
chai.should();
describe('cities', () => {
describe('GET /cities', () => {
it('should get all cities', (done) => {
chai.request(app)
.get('/cities')
.end((error, response) => {
response.should.have.status(200);
response.body.should.be.a('array');
expect(response.body[0]).to.have.property('name');
expect(response.body[0]).to.have.property('altitude');
expect(response.body[0]).to.have.property('population');
expect(response.body[0]).to.have.property('foundationDate');
expect(response.body[0]).to.have.property('age');
expect(response.body[0]).to.have.property('area');
expect(response.body[0]).to.have.property('density');
expect(response.body[0].name).to.equal('Zaragoza');
expect(response.body[1].name).to.equal('Madrid');
done();
});
});
});
});
* Caso de uso: **Registro (201 Created)**
it('should register a new city', (done) => {
chai.request(app)
.post('/cities')
.send({
name: 'cityName',
population: 100,
altitude: 200,
foundationDate: '2005-10-10',
area: 1000
})
.end((error, response) => {
response.should.have.status(201);
expect(response.body).to.have.property('id');
expect(response.body).to.have.property('name');
expect(response.body).to.have.property('population');
expect(response.body).to.have.property('altitude');
expect(response.body).to.have.property('foundationDate');
expect(response.body).to.have.property('age');
expect(response.body).to.have.property('area');
expect(response.body).to.have.property('density');
done();
});
});
* Caso de uso: **Validación (400 Bad Request)**
it('validation should fail because name is mandatory', (done) => {
chai.request(app)
.post('/cities')
.send({
population: 100,
altitude: 200,
foundationDate: '2005-10-10',
area: 1000
})
.end((error, response) => {
response.should.have.status(400);
expect(response.body.status).to.equal('bad-request');
expect(response.body.message).to.equal('name field is mandatory');
done();
});
});
. . .
"scripts": {
"integration-test": "mocha src/test/integration --exit",
. . .
. . .
También podemos aprovechar para modificar el script ''test'' de forma que lance todos los tests: unitarios y de integración:
. . .
"scripts": {
. . .
"test": "npm run unit-test && npm run integration-test",
. . .
. . .
Asi, podremos lanzar todos los tests solo con ''npm test''
===== Crear un entorno de pruebas con Docker =====
Colocaremos en la carpeta ''db'' del proyecto el script SQL que creará la base de datos y las tablas. También podemos añadir las sentencias ''INSERT'' que queramos para preparar datos según los casos de nuestros tests de integración.
Podemos crear un fichero ''docker-compose.dev.yaml'' con la siguiente configuración:
version: "3.4"
name: cities
services:
db:
image: mariadb:11.3.2
container_name: cities-db-dev
environment:
MYSQL_USER: 'user'
MYSQL_PASSWORD: 'password'
MYSQL_PORT: 3306,
MYSQL_ROOT_PASSWORD: 'rootpassword'
ports:
- "3306:3306"
volumes:
- ./db:/docker-entrypoint-initdb.d
Y podremos lanzar nuestro entorno de pruebas con el siguiente comando:
santi@zenbook:$ docker compose -f docker.compose.dev.yaml up -d
Para destruir el entorno:
santi@zenbook:$ docker compose -f docker.compose.dev.yaml down
También se puede simplemente detener:
santi@zenbook:$ docker compose -f docker.compose.dev.yaml stop
O bien iniciar si está detenido:
santi@zenbook:$ docker compose -f docker.compose.dev.yaml start
====== API Testing con Postman ======
{{postman-logo.png?100 }}
===== Test scripts =====
En la propia documentación de Postman hay un montón de ejemplos de todo lo que es posible hacer con los test scripts de esta herramienta:
* https://www.postman.com/api-platform/api-testing/
* https://learning.postman.com/docs/writing-scripts/script-references/test-examples/
* https://learning.postman.com/docs/writing-scripts/script-references/variables-list/
* https://learning.postman.com/docs/writing-scripts/script-references/postman-sandbox-api-reference/
===== Newman =====
Instalar [[https://learning.postman.com/docs/collections/using-newman-cli/command-line-integration-with-newman|Newman]] de forma global como comando en tu equipo, listo para ejecutarse directamente desde la consola (Si aún no tienes Node.js instalado, sigue [[https://docs.npmjs.com/downloading-and-installing-node-js-and-npm|estas instrucciones]]):
santi@zenbook:$ npm install -g newman
Cómo lanzar una colección de Postman con Newman:
santi@zenbook:$ newman run api-collection.postman_collection.json
Cómo lanzar una colección de Postman con Newman para un entorno determinado:
santi@zenbook:$ newman run -e localhost.postman_environment.json api-collection.postman_collection.json
Puedes encontrar una colección de ejemplo ya preparada con algunos scripts, lista para ser ejecutada con Newman [[https://github.com/codeandcoke/cities-api/blob/main/cities.postman_collection.json|aqui]]
===== API testing con Postman automatizado con GitHub Actions =====
==== Dockerfile ====
El primer paso será generar la imagen docker de la API que hemos desarrollado, para que podamos lanzarla luego como un contenedor docker
FROM node:22-alpine
WORKDIR /usr/src/app
COPY package*json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
ENV NODE_ENV=production
CMD ["node", "src/app.js"]
==== Docker compose ====
El siguiente es un ejemplo de un fichero ''docker-compose.yml'' listo para lanzar la API + Base de datos como contenedores docker
version: "3.4"
name: cities
services:
db:
image: mariadb:11.3.2
container_name: cities-db
environment:
MARIADB_USER: 'user'
MARIADB_PASSWORD: 'password'
MARIADB_PORT: 3306,
MARIADB_ROOT_PASSWORD: 'rootpassword'
volumes:
- ./db:/docker-entrypoint-initdb.d
healthcheck:
test: [ CMD, healthcheck.sh, --connect, --innodb_initialized]
interval: 10s
timeout: 5s
retries: 3
networks:
- network1
backend:
image: cities-be
container_name: cities-api
ports:
- "8080:8080"
depends_on:
db:
condition: service_healthy
networks:
- network1
frontend:
image: cities-fe
container_name: cities-ui
ports:
- "1234:1234"
networks:
- network1
networks:
network1:
driver: bridge
==== GitHub action ====
Por último, configuramos un workflow de GitHub Actions que hará lo siguiente:
* Generamos la imagen docker de nuestra API
* Lanzará nuestra API + Base de datos como contenedores docker
* Instalará Newman
* Lanzará la colección con los tests con Newman sobre la API que se acaba de lanzar
name: Integration tests with Newman
on:
pull_request:
branches:
- main
jobs:
newman:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20.x, 22.x, 24.x]
steps:
- uses: actions/checkout@master
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm install
- run: docker build -t cities-api .
- run: docker compose up -d
- run: npm install -g newman
- run: newman run cities.postman_collection.json -e local.postman_environment.json
====== Proyectos de ejemplo ======
En [[https://github.com/codeandcoke/cities|cities]] puedes encontrar un proyecto con tests unitarios y de integración implementados. También está lo necesario para crear un entorno de pruebas y lanzar todos los tests utilizando dicho entorno.
----
(c) 2025-2026 Santiago Faci