Skip to main content

Command Palette

Search for a command to run...

Introduccion al testing con nodejs + typescript + jest

Updated
β€’5 min read
Introduccion al testing con nodejs + typescript + jest

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

ci/cd

La extension para coverage, en caso que quieran despues hacerlo bloqueante

coverage

Demostracion de como corren los TEST

pipeline

More from this blog

J

jd-apprentice - blog

29 posts

🧰 devops | πŸ’» tech | πŸ“š linux | πŸ’– anime