@tresdoce-nestjs-toolkit/test-utils v2.0.1
Esta librería está pensada para ser utilizada en 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
- 📝 Requerimientos básicos
- 🛠️ Instalar dependencia
- 👨💻 Uso
- 😝 CreateMock
- 🧪 TestContainers
- 📄 Changelog
- 📜 License MIT
📝 Requerimientos básicos
- NestJS Starter
- Node.js v18.17.0 or higher (Download)
- YARN v1.22.18 or higher
- NPM v9.6.7 or higher
- NestJS v10.3.0 or higher (Documentación)
🛠️ Instalar dependencia
npm install -D @tresdoce-nestjs-toolkit/test-utils
yarn add -D @tresdoce-nestjs-toolkit/test-utils
👨💻 Uso
Base Configuration for test
//...
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
//...
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();
});
//...
});
😝 CreateMock
CreateMock
es una función que facilita la creación de mocks para peticiones HTTP utilizando Nock como base.
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
createMock({
url: 'http://example.com/api/data',
method: 'get',
statusCode: 200,
responseBody: { success: true },
});
Response body as string
createMock({
url: 'http://example.com/api/message',
method: 'get',
statusCode: 200,
responseBody: 'Success message',
});
Response body as Buffer
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.
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.
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
.
createMock({
url: 'http://example.com/api/dynamicData',
method: 'get',
statusCode: 200,
responseBody: readFixtureFile('../path/to/fixture.json'),
});
🧪 TestContainers
TestContainers 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 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
)
//./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.
//./jest.globalSetup.ts
import { initDockerCompose } from '@tresdoce-nestjs-toolkit/test-utils';
const services = ['mongo', 'redis', 'elasticsearch'];
module.exports = initDockerCompose(services);
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'
//./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);
//./jest.globalTeardown.ts
import { closeDockerCompose } from '@tresdoce-nestjs-toolkit/test-utils';
module.exports = closeDockerCompose({ removeVolumes: false });
# ./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.
//...
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 localhostgetName()
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.
// 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.
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.
4 months ago
8 months ago
9 months ago
9 months ago
6 months ago
10 months ago
10 months ago
10 months ago
7 months ago
6 months ago
9 months ago
10 months ago
10 months ago
10 months ago
10 months ago
9 months ago
9 months ago
8 months ago
9 months ago
9 months ago
9 months ago
10 months ago
12 months ago
12 months ago
12 months ago
12 months ago
12 months ago
12 months ago
12 months ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago