En este blog sencillo voy a mostrar parte de la configuracion y como se ven los test cuando usamos node + express, en una aplicacion con typescript y jest.
El repositorio que estoy utilizando es un proyecto random que tengo waifuland
Configuracion ๐งฐ
En caso que llegue a ser JS solamente la configuracion es mas simple.
โฃ ๐ .env
โฃ ๐ .gitignore
โฃ ๐ jest.config.js
โฃ ๐ jest.setup.js
โฃ ๐ package-lock.json
โฃ ๐ package.json
โฃ ๐ tsconfig.build.json
โ ๐ tsconfig.json
El testMatch ajustenlo a su necesidad, si no usan TS pueden quitar el transform.
// jest.config.js
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
clearMocks: true,
coverageProvider: "v8",
moduleFileExtensions: ["js", "jsx", "ts", "tsx", "json", "node"],
setupFilesAfterEnv: ["<rootDir>/jest.setup.js"],
roots: ["<rootDir>/src"],
testMatch: ["**/__tests__/(unit|integration)/*.[jt]s?(x)", "**/?(*.)+(spec|test).[tj]s?(x)"],
transform: {
"^.+\\.(ts|tsx)$": "ts-jest",
},
};
// jest.setup.js
jest.setTimeout(30000);
Dependencias ๐
ts-jest
No es necesario si solo se usa Javascript.
"scripts": {
"test": "jest",
"test:coverage": "jest --coverage"
},
"devDependencies": {
"@jest/globals": "^29.7.0",
"@types/jest": "^29.5.11",
"@types/node": "^17.0.8",
"@types/supertest": "^6.0.2",
"jest": "^29.7.0",
"supertest": "^6.3.4",
"ts-jest": "^29.1.1",
"typescript": "^5.3.3"
}
Pipeline ๐งช
Los env no es necesario si no tienen datos sensibles, yo por que tengo el connection string de la base de datos y mis credenciales de rollbar.
name: CI/CD
on:
workflow_dispatch:
push:
branches:
- master
- development
paths:
- 'src/**'
- 'Dockerfile'
pull_request:
branches:
- master
- development
paths:
- 'src/**'
- 'Dockerfile'
env:
BRANCH_NAME: ${{ github.ref_name }}
jobs:
gitleaks:
uses: jd-apprentice/jd-workflows/.github/workflows/gitleaks.yml@main
with:
runs-on: ubuntu-latest
name: Gitleaks
secrets:
gh_token: ${{ secrets.GITHUB_TOKEN }}
test:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '18'
- run: npm install
- name: Run tests
run: |
touch .env
echo DB_HOST="${{ secrets.DB_HOST }}" >> .env
echo PORT=3000 >> .env
echo NODE_ENV=TEST >> .env
echo ROLLBAR_TOKEN="${{ secrets.ROLLBAR_TOKEN }}" >> .env
echo ROLLBAR_ENVIRONMENT=development >> .env
npm run test
- uses: ArtiomTr/jest-coverage-report-action@v2.3.0
Tipos de TEST ๐งช
Aca les voy a dejar ejemplos de test UNITARIOS y de INTEGRACION, la idea de los unitarios es mockear o simular los diferentes metodos, funciones y demas. Sin probar la integracion completa, se suelen harcodear datos tanto de input como output si es necesario.
Ejemplo de Unitario ๐ฐ
๐ณ unit/
โฃ ๐ mocks/
โ โ ๐ image.ts
โ ๐ images.test.ts
El mock este es un ejemplo de una de mis apps
// mocks/image.ts
export const image = {
id: expect.any(String),
url: expect.any(String),
is_nsfw: expect.any(Boolean),
tag: {
name: expect.any(String),
tag_id: expect.any(Number),
description: expect.any(String),
is_nsfw: expect.any(Boolean),
createdAt: expect.any(String),
updatedAt: expect.any(String),
is_active: expect.any(Boolean)
}
};
const defaultSize = 1;
const defaultId = '1';
const defaultUrl = 'https://www.example.com/image.jpg';
const defaultNsfw = false;
const defaultTag = {
name: 'tag',
tag_id: 1,
description: 'description',
is_nsfw: false,
createdAt: '2021-01-01',
updatedAt: '2021-01-01',
is_active: true
};
const defaultImage = {
id: defaultId,
url: defaultUrl,
is_nsfw: defaultNsfw,
tag: defaultTag
};
export const getImageMock = jest.fn((args = {
size: defaultSize,
tag_id: defaultTag.tag_id
}) => {
let response = Array.from({ length: args.size }, () => defaultImage);
if (args.tag_id) {
response = response.map((image) => {
return {
...image,
tag: {
...image.tag,
tag_id: args.tag_id,
name: expect.any(String),
description: expect.any(String)
}
};
});
};
return args.size === 1 ? response[0] : response;
});
// image.test.ts
import { ImageProp } from "../../interfaces/image-interface";
import { getImageMock, image } from "./mocks/image";
describe("UNIT - Images Module", () => {
beforeAll(async () => {
jest.resetModules();
});
test('GET /api/images - when asking for an image should return a random one', async () => {
const response = getImageMock({ size: 1 });
expect(getImageMock).toHaveBeenCalledTimes(1);
expect(response).toMatchObject(image);
});
test('GET /api/images?size=2 - when asking for 2 images should return an array of 2 images', async () => {
const response = getImageMock({ size: 2 });
expect(getImageMock).toHaveBeenCalledTimes(1);
expect(response).toEqual([image, image]);
});
test('GET /api/images?size=1&tag_id=2 - when asking for a tag_id should return that one', async () => {
const response = getImageMock({ size: 1, tag_id: 2 }) as ImageProp;
expect(getImageMock).toHaveBeenCalledTimes(1);
expect(response.tag.tag_id).toBe(2);
});
test("GET /api/images/all - when asking for all images should return an array of images", async () => {
const response = getImageMock({ size: 4 }) as ImageProp[];
expect(getImageMock).toHaveBeenCalledTimes(1);
expect(response.length).toBe(4);
});
});
Ejemplo de Integracion ๐ฐ
Los test de INTEGRACION son los que prueban algo entero, ej un controlador, como usamos supertest para esto lo que hacemos es hacer llamados HTTP, levantando la app por eso cargo la DB, de ahi le voy haciendo request reales, asi que no hagan esto en PROD, solamente en entornos bajos.
import { loadDatabase } from "../../../app/db";
import Config from "../../../app/config/config";
import { app } from "../../../app/main";
import request from 'supertest';
import { Response } from 'supertest';
import { Server } from 'http';
const baseRoute = '/api/images';
const contentTypeKey = 'Content-Type';
const contentTypeValue = /json/;
const httpSuccess = 200;
export const image = {
id: expect.any(String),
url: expect.any(String),
is_nsfw: expect.any(Boolean),
tag: {
name: expect.any(String),
tag_id: expect.any(Number),
description: expect.any(String),
is_nsfw: expect.any(Boolean),
createdAt: expect.any(String),
updatedAt: expect.any(String),
is_active: expect.any(Boolean)
}
}
describe("INTEGRATION - Images Module", () => {
let server: Server;
beforeAll(async () => {
jest.resetModules();
});
beforeEach(async () => {
server = app.listen(Config.app.port);
await loadDatabase(Config.db.uri);
})
test('GET /api/images - when asking for an image should return a random one', async () => {
await request(app)
.get(baseRoute)
.expect(contentTypeKey, contentTypeValue)
.expect(httpSuccess)
.then((response) => expectImage(response));
});
}
function expectImage(response: Response) {
expect(response.body).toBeTruthy();
expect(response.body).toMatchObject(image);
}
Resultados ๐
Esta es mi pipeline que incluye gitleaks, jest, codacy y dockerhub
La extension para coverage, en caso que quieran despues hacerlo bloqueante
Demostracion de como corren los TEST