====== 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