UNPKG

@tresdoce-nestjs-toolkit/test-utils

Version:
475 lines (370 loc) 13.5 kB
<div align="center"> <img alt="nestjs-logo" width="150" height="auto" src="https://raw.githubusercontent.com/tresdoce/tresdoce-nestjs-toolkit/master/.readme-static/iso-nestjs.svg" /> <h1>Tresdoce NestJS Toolkit<br/>Test-Utils</h1> </div> <div align="center"> <img src="https://img.shields.io/static/v1.svg?style=flat&label=NodeJS&message=v20.19.3&labelColor=339933&color=757575&logoColor=FFFFFF&logo=Node.js" alt="NodeJS"/> <img src="https://img.shields.io/static/v1.svg?style=flat&label=NPM&message=v11.4.2&labelColor=CB3837&logoColor=FFFFFF&color=757575&logo=npm" alt="NPM"/> <img src="https://img.shields.io/static/v1.svg?style=flat&label=NestJS&message=v11.1.6&labelColor=E0234E&logoColor=FFFFFF&color=757575&logo=Nestjs" alt="NestJS"/><br/> <img src="https://img.shields.io/github/license/tresdoce/tresdoce-nestjs-toolkit?style=flat" alt="GitHub license" > <img alt="Release" src="https://img.shields.io/npm/v/@tresdoce-nestjs-toolkit/test-utils.svg"> <br/> </div> <br/> Esta librería está pensada para ser utilizada en [NestJS Starter](https://github.com/rudemex/nestjs-starter) o en este monorepo de funcionalidades, o cualquier proyecto que utilice una configuración centralizada, siguiendo la misma arquitectura del starter. Al momento de realizar nuestros test, puede existir la necesidad de implementar una configuración para el `ConfigModule` , lo cual a veces se vuelve tedioso tener que estar creando un mock puntual para cada test, generando además duplicidad de código, como asi también la utilización de algún servicio que requiera nuestro código, como puede ser una Base de datos para test, y no tener la infraestructura disponible para dichas pruebas. Por esta razón, y con el fin de desarrollar nuestros test de manera más ágiles y sin preocupaciones, surge la idea de esta librería que maneja de manera centralizada todo lo necesario para nuestros tests. ## Glosario - [🥳 Demo](https://nestjs-starter.tresdoce.com.ar/v1/docs) - [📝 Requerimientos básicos](#basic-requirements) - [🛠️ Instalar dependencia](#install-dependencies) - [👨‍💻 Uso](#use) - [😝 CreateMock](#create-mock) - [🧪 TestContainers](#testcontainers) - [📄 Changelog](./CHANGELOG.md) - [📜 License MIT](./license.md) --- <a name="basic-requirements"></a> ## 📝 Requerimientos básicos - [NestJS Starter](https://github.com/rudemex/nestjs-starter) - Node.js v20.19.3 or higher ([Download](https://nodejs.org/es/download/)) - YARN ≥ 1.22.22 o NPM ≥ 11.4.2 - NestJS v11.1.6 or higher ([Documentación](https://nestjs.com/)) <a name="install-dependencies"></a> ## 🛠️ Instalar dependencia ``` npm install -D @tresdoce-nestjs-toolkit/test-utils ``` ``` yarn add -D @tresdoce-nestjs-toolkit/test-utils ``` <a name="use"></a> ## 👨‍💻 Uso ### Base Configuration for test ```typescript //... import { config } from '@tresdoce-nestjs-toolkit/test-utils'; describe('Suite for base config', () => { let app: INestApplication; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ imports: [ ConfigModule.forRoot({ isGlobal: true, load: [config], }), //... ], }).compile(); app = module.createNestApplication(); await app.init(); }); //... }); ``` ### Dynamic Configuration for test ```typescript //... import { dynamicConfig } from '@tresdoce-nestjs-toolkit/test-utils'; describe('Suite for dynamic config', () => { let app: INestApplication; const args = { httOptions: { timeout: 5000, maxRedirects: 5, }, }; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ imports: [ ConfigModule.forRoot({ isGlobal: true, load: [dynamicConfig(args)], }), //... ], }).compile(); app = module.createNestApplication(); await app.init(); }); //... }); ``` <a name="create-mock"></a> ## 😝 CreateMock `CreateMock` es una función que facilita la creación de mocks para peticiones HTTP utilizando **Nock** como base. ```typescript import { createMock, cleanAllMock } from '@tresdoce-nestjs-toolkit/test-utils'; describe('MyController', () => { beforeEach(async () => { //... cleanAllMock(); }); //... it('should be return user data from mock', async () => { createMock({ url: 'https://test.com/api/user/1', method: 'get', statusCode: 200, responseBody: { id: 1, firstName: 'John', lastName: 'Doe', email: 'john.doe@email.com', }, }); const user = await controller.getUser(); //console.log(user); //Return mock response expect(user).toHaveProperty('firstName', 'John'); expect(user).toHaveProperty('lastName', 'Doe'); expect(user).toHaveProperty('email', 'john.doe@email.com'); }); //... }); ``` El `responseBody` del `createMock` admite también otros tipos de respuesta además de `JSON`. #### Response body as JSON ```typescript createMock({ url: 'http://example.com/api/data', method: 'get', statusCode: 200, responseBody: { success: true }, }); ``` #### Response body as string ```typescript createMock({ url: 'http://example.com/api/message', method: 'get', statusCode: 200, responseBody: 'Success message', }); ``` #### Response body as Buffer ```typescript createMock({ url: 'http://example.com/api/file', method: 'get', statusCode: 200, responseBody: Buffer.from('Some binary data'), }); ``` #### Response body as function Esta funcionalidad es ideal para tener fixtures de respuestas en archivos json y retornarlos. ```typescript createMock({ url: 'http://example.com/api/dynamicData', method: 'get', statusCode: 200, responseBody: () => JSON.parse(fs.readFileSync(path.resolve(__dirname, '../path/to/fixture.json'), 'utf8')), }); ``` En el caso de que tengas muchos fixtures y asi evitar duplicidad de código, pódes abstraer la función que retorna el `JSON` con el siguiente código. ```typescript const readFixtureFile = (filePath: string) => { const absolutePath = path.resolve(__dirname, filePath); const fileContents = fs.readFileSync(absolutePath, 'utf8'); return JSON.parse(fileContents); }; ``` y luego pódes utilizarlo de la siguiente manera, ya que la respuesta es un `JSON`. ```typescript createMock({ url: 'http://example.com/api/dynamicData', method: 'get', statusCode: 200, responseBody: readFixtureFile('../path/to/fixture.json'), }); ``` <a name="testcontainers"></a> ## 🧪 TestContainers [TestContainers](https://node.testcontainers.org/) es una librería que utiliza docker de por medio para poder instanciar un servicio durante el entorno de testing, tanto local como asi también en nuestros pipelines, y poder realizar las pruebas correctamente sin tener que estar consumiendo el servicio de algún entorno. Esta librería viene con una configuración base para instanciar `Redis`, `MongoDB`, `Postgres`, `MySql` y `Elasticsearch`, pero también cuenta con la posibilidad de levantar cualquier otro servicio utilizando las imágenes de docker, por lo que se requiere tener [Docker](https://www.docker.com/) instalado. ### Global Container Instancia uno o más containers a partir de un archivo `docker-compose.yml`, está funcionalidad es ideal para instanciar los servicios de una aplicación y que disponible para consumir en todos los test. Agregar `globalSetup` y `globalTeardown` a la configuración de jest de la aplicación (`jest.config.ts`) ```typescript //./jest.config.ts import { jestConfig } from '@tresdoce-nestjs-toolkit/commons'; import * as dotenv from 'dotenv'; process.env.NODE_ENV = 'test'; dotenv.config({ path: '.env.test', }); module.exports = { ...jestConfig(), globalSetup: './jest.globalSetup.ts', globalTeardown: './jest.globalTeardown.ts', }; ``` Creamos los archivos `jest.globalSetup.ts` y `jest.globalTeardown.ts` en el root de la aplicación. ```typescript //./jest.globalSetup.ts import { initDockerCompose } from '@tresdoce-nestjs-toolkit/test-utils'; const services = ['mongo', 'redis', 'elasticsearch']; module.exports = initDockerCompose(services); ``` <details> <summary>💬 Para ver en detalle todas las propiedades de la configuración, hace clic acá.</summary> La función `initDockerCompose` recibe tres parámetros. `services`: Es un array de string que sirve para especificar que servicios se quiere instanciar del `docker-compose.yml`, si no se envía el parámetro o se envía un array vacío, se inicializa todos los servicios definidos en el archivo. - Type: `String[]` - Required: `false` - Default: `[]` - Example: `['mongo', 'redis', 'elasticsearch']` `composeFilePath`: Es el path de donde se encuentra el archivo `docker-compose.yml`. - Type: `String` - Required: `false` - Default: `'.'` `composeFile`: Es el nombre del archivo de `docker-compose`. - Type: `String` - Required: `false` - Default: `'docker-compose.yml'` ```typescript //./jest.globalSetup.ts import { initDockerCompose } from '@tresdoce-nestjs-toolkit/test-utils'; import * as path from 'path'; const services = ['mongo', 'redis']; const composeFilePath = path.resolve(__dirname, 'fixtures', 'docker-compose'); const composeFile = 'docker-compose-test.yml'; module.exports = initDockerCompose(services, composeFilePath, composeFile); ``` </details> ```typescript //./jest.globalTeardown.ts import { closeDockerCompose } from '@tresdoce-nestjs-toolkit/test-utils'; module.exports = closeDockerCompose({ removeVolumes: false }); ``` ```yaml # ./docker-compose.yml version: '3.9' services: mongo: image: mongo:5.0 container_name: local-mongo restart: always ports: - '27017:27017' environment: TZ: 'America/Argentina/Buenos_Aires' MONGO_INITDB_ROOT_USERNAME: root MONGO_INITDB_ROOT_PASSWORD: 123456 MONGO_INITDB_DATABASE: test_db redis: image: redis:6.2-alpine container_name: local-redis restart: always ports: - '6379:6379' environment: TZ: 'America/Argentina/Buenos_Aires' REDIS_PORT: 6379 REDIS_PASSWORD: 123456 REDIS_HOST: cache command: ['redis-server', '--appendonly', 'yes', '--requirepass', '123456'] ``` > ⚠️ En caso de fallas al correr en los pipelines, revisar que el `docker-compose.yml` este bien configurado y que el > host del runner sea el mismo que usa docker. Ej.: http://localhost:6379 o http://docker:6379 ### Generic Container Instancia un container con la imagen del servicio, está pensada para utilizarse para proyectos que utilizan un solo servicio. ```typescript //... import { TCPostgresOptions, testContainers, delay } from '@tresdoce-nestjs-toolkit/test-utils'; jest.setTimeout(70000); describe('TypeOrm - Postgres', () => { let app: INestApplication; let container: testContainers; // Instanciamos el test container beforeAll(async () => { // await delay(30000); // delay para inicializar el container container = await new testContainers('postgres:13', TCPostgresOptions); await container.start(); }); // Apagamos el container afterAll(async () => { await container.stop({ removeVolumes: true }); }); beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ //... }).compile(); app = module.createNestApplication(); await app.init(); }); afterEach(async () => { await app.close(); }); //... }); ``` La clase `testContainers` requiere de dos parámetros, donde el primero es la imagen de docker junto a su tag, y el segundo son las configs para ese container. **Schema:** `new testContainers('<img-docker>:<tag-img-docker>', { config-container })` Para la configuración del contenedor, tiene disponibles las siguientes opciones. ``` { ports: [{ container: number, host: number }], envs: { KEY: value //... }, containerName: string, startupTimeout: number, reuse: boolean } ``` La clase `testContainers` cuenta con algunas funciones que retorna información del contenedor. - `getEnvs()` retorna las variables de entorno enviadas al contenedor. - `getContainer()` retorna el contenedor instanciado. - `getHost()` retorna el host del contenedor instanciado, esto es util, ya que a veces el contenedor no se hostea en **localhost** - `getName()` retorna el nombre del contenedor. ### Troubleshooting Para solucionar el problema de `failed: port is already allocated`, es recomendable cambiar el puerto del `host`, manteniendo el del `container` con el default. ```typescript // Ejemplo para MongoDB await new testContainers('mongo:5.0', { ...TCMongoOptions, ports: [ { container: 27017, host: 27013, }, ], }); ``` Limpiar los containers, images y volumes para probar en un entorno desde cero. ```bash docker system prune --volumes docker system prune -a yarn test --force ``` ## 📄 Changelog Todos los cambios notables de este paquete se documentarán en el archivo [Changelog](./CHANGELOG.md). --- <div align="center"> <a href="mailto:mdelgado@tresdoce.com.ar" target="_blank" alt="Send an email"> <img src="https://raw.githubusercontent.com/tresdoce/tresdoce-nestjs-toolkit/ab924d5bdd9a9b9acb3ca5721d4ce977c6b7f680/.readme-static/logo-mex-red.svg" width="120" alt="Logo - Mex" /> </a><br/> <p>Made with ❤</p> </div>