====== Testing ======
{{ testing.png }}
Testing (Fuente: https://katalon.com/resources-center/blog/unit-testing)
===== Test unitarios =====
==== 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 =====
* 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
====== 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 Santiago Faci