Tests
En un segundo stage, vamos a ejecutar tests. Podemos ejecutar varios tipos de tests, pero vamos a comenzar por el primero, que son los tests unitarios. En desarrollo es buena práctica generar los tests unitarios del código. ¿No vamos a generar una imagen sin probar el build antes verdad?
Pero podemos probar mucho más que los tests unitarios, por ejemplo:
- Calidad de código
- Issues de seguridad
- Paquetes con CVEs
- Linter para el código
- etc
¿Para qué vamos a construir el código si ya tiene problema con linter? Entonces este proceso debería venir antes del build para ni perder tiempo. Vamos a ejecutar el test unitario en este momento.
Para que ejecutemos un test unitario es necesario desarrollarlo, pero el proyecto en cuestión ya tiene el test unitario que vamos a utilizar. No vamos a entrar en detalles sobre eso, solo mostrar cómo ejecutar para nuestro job.
Tests unitarios se desarrollan para probar un pequeño trozo aislado de código, generalmente una función o método. Si los valores de entrada generan la salida esperada entonces el test pasa.
❯ tree src
src
├── App.css
├── App.jsx
├── App.test.jsx # Este es el test para App.jsx
├── assets
│ ├── gitlab.svg
│ └── react.svg
├── index.css
└── main.jsx
2 directories, 7 files
# Ejecutando test unitario.
npm test
> [email protected] test
> vitest
DEV v3.1.2 /Users/davidprata/Desktop/gitlab/learn-gitlab-app
✓ src/App.test.jsx (3 tests) 27ms
✓ an always true assertion (1)
✓ should be equal to 2 1ms
✓ App (2)
✓ renders the App component 22ms
✓ shows the GitLab logo 3ms
Test Files 1 passed (1)
Tests 3 passed (3)
Start at 09:26:10
Duration 920ms (transform 34ms, setup 190ms, collect 33ms, tests 27ms, environment 409ms, prepare 48ms)
JUNIT report written to /Users/davidprata/Desktop/gitlab/learn-gitlab-app/reports/junit.xml
HTML Report is generated
You can run npx vite preview --outDir reports/html to see the test results.
PASS Waiting for file changes...
press h to show help, press q to quit
Entonces vamos a poner esto en nuestro pipeline. Ejecutar el comando npm test depende de algunos paquetes que se instalan y por eso es necesario ejecutar el npm ci antes.
default:
tags:
- general
stages:
- build
build:
stage: build
image: node:22-slim
script:
- node --version
- npm --version
- npm ci # Instalación de las dependencias
- npm run build # Generará la carpeta .build
- echo "ACCESS_TOKEN=abc123" > .env # Un test solo para los reports
artifacts:
when: on_success # Ya es el patrón
expire_in: "1 hour"
paths:
- build/ # La carpeta toda sin exclusiones
reports:
dotenv: .env
unit-test:
stage: test
image: node:22-slim
script:
- echo "ACCESS_TOKEN ES IGUAL A $ACCESS_TOKEN" # Test ver si recibió la variable del job anterior.
- npm ci # Instalación de las dependencias
- npm test
Al subir esto tenemos...

O sea, no podemos definir un stage stage: test si no está definido dentro de stages:.
❯ git add .gitlab-ci.yml
❯ git cm "add stage test fixed"
❯ git push origin pipe/test
Y tenemos el log.
Using docker image sha256:a7bca975c7f3a862dc60f3d8aaa3862fce3208066dd3567f060381b506f38402 for node:22-slim with digest node@sha256:157c7ea6f8c30b630d6f0d892c4f961eab9f878e88f43dd1c00514f95ceded8a ...
$ echo "ACCESS_TOKEN ES IGUAL A $ACCESS_TOKEN"
ACCESS_TOKEN ES IGUAL A abc123 # La variable de entorno está aquí.
$ npm ci
added 440 packages, and audited 441 packages in 27s
152 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
$ npm test
> [email protected] test
> vitest
RUN v3.1.2 /builds/puziol/learn-gitlab-app
✓ src/App.test.jsx > an always true assertion > should be equal to 2 3ms
✓ src/App.test.jsx > App > renders the App component 82ms
✓ src/App.test.jsx > App > shows the GitLab logo 16ms
Test Files 1 passed (1)
Tests 3 passed (3)
Start at 12:38:02
Duration 5.05s (transform 313ms, setup 543ms, collect 168ms, tests 103ms, environment 1.85s, prepare 692ms)
JUNIT report written to /builds/puziol/learn-gitlab-app/reports/junit.xml
HTML Report is generated
You can run npx vite preview --outDir reports/html to see the test results.
Cleaning up project directory and file based variables
00:01
Job succeeded
Consejo de Seguridad
Tanto los reports como paths se inyectan automáticamente en el próximo job. Esta demostración se hizo para que variables de entorno necesarias en un job puedan utilizarse en otros jobs también. Pero existe un problema grave aquí y debemos prestar atención siempre que algún pipeline sufra alteraciones y necesitemos un review.
script:
- node --version
- npm --version
- npm ci
- npm run build
- echo "ACCESS_TOKEN=abc123" >> .env
- echo "SECRET=$SECRET" >> .env # Estamos poniendo la variable SECRET (masked) del repositorio dentro del .env.
El enmascaramiento protege de aparecer en la salida de la consola, pero no de aparecer en los artefactos, entonces ten cuidado. Aquí el valor del artefacto.
ACCESS_TOKEN=abc123
SECRET=platform-engineer-is-the-new-devops
Ejecución en Paralelo
Podemos hacer varios tests al mismo tiempo y ganar velocidad. No necesitamos esperar que un test acabe para comenzar otro. Jobs dentro del mismo stage se ejecutan en paralelo si no hay dependencias entre ellos. Hablaremos de dependencias más tarde.
Tenemos el archivo eslint.config.js en el repositorio, vamos también a crear un lint con él. Para ejecutar esto necesitamos ejecutar el npm run lint
ESLint es una herramienta de linting para JavaScript (y otros archivos, como JSX, TypeScript, etc.), y su principal función es analizar el código para identificar patrones o prácticas que pueden mejorarse, o que puedan causar errores. Verifica el código en busca de problemas que van desde errores de sintaxis hasta prácticas de codificación inconsistentes o problemáticas. ESLint ayuda a garantizar que el código sea limpio, comprensible y libre de errores comunes.
Me gusta bastante la idea de utilizar linters para que el código no salga de la formatación adecuada principalmente.
default:
tags:
- general
stages: # Podríamos eliminar pues los dos stages de abajo son patrones de gitlab, pero es buena práctica mantener.
- build
- test
build:
stage: build
image: node:22-slim
script:
- node --version
- npm --version
- npm ci
- npm run build
artifacts:
when: on_success
expire_in: "1 hour"
paths:
- build/
unit-test:
stage: test # mismo stage
image: node:22-slim
script:
- npm ci
- npm test
lint-test:
stage: test # mismo stage
image: node:22-slim
script:
- npm ci
- npm run lint
Solo para recordar, estamos utilizando stages patrón de GitLab (build y test), luego ni necesitaríamos definir stages: [build, test]. Si omitimos esto funcionaría normalmente.
Subiendo y probando.
❯ git add .gitlab-ci.yml
❯ git commit -m "add test lint and remove reports"
❯ git push origin pipe/test

Observa que los jobs están ejecutándose en paralelo, pues comenzaron al mismo tiempo, pero falló el job lint-test con este error.
$ npm run lint
> [email protected] lint
> eslint -f json -o gl-codequality.json . # Este es el comando que se está ejecutando.
Oops! Something went wrong! :(
ESLint: 9.25.1
TypeError: Expected lint-test.artifacts.reports.codequality to be one exact path, got: undefined
at getOutputPath (/builds/puziol/learn-gitlab-app/node_modules/eslint-formatter-gitlab/index.js:51:11)
at eslintFormatterGitLab (/builds/puziol/learn-gitlab-app/node_modules/eslint-formatter-gitlab/index.js:268:54)
at Object.format (/builds/puziol/learn-gitlab-app/node_modules/eslint/lib/eslint/eslint.js:1054:12)
at printResults (/builds/puziol/learn-gitlab-app/node_modules/eslint/lib/cli.js:365:33)
at async Object.execute (/builds/puziol/learn-gitlab-app/node_modules/eslint/lib/cli.js:719:4)
at async main (/builds/puziol/learn-gitlab-app/node_modules/eslint/bin/eslint.js:160:19)
Cleaning up project directory and file based variables
00:00
ERROR: Job failed: exit code 1
Cuando ejecutamos npm run lint en realidad ejecutó el comando "eslint -f json -o gl-codequality.json ." y esto está dentro de packages.json
{
"scripts": {
"dev": "vite",
"build": "vite build",
"test": "vitest",
"e2e": "npx playwright test",
"lint": "eslint -f json -o gl-codequality.json .",
"preview": "vite preview"
},
}
Si alteramos para eslint . el job pasará. Haz eso y sube nuevamente. Cuando hablemos mejor sobre reports volveremos al patrón. Lo que importa aquí es ejecutar cosas en paralelo.
Otro test que podríamos hacer es sobre seguridad en los paquetes utilizados usando el npm audit. Snyk también es una herramienta muy popular para buscar vulnerabilidades de paquetes y tenemos muchas otras.
Al utilizar el comando npm audit --json > vulnerability-report.json estamos guardando la salida al archivo y podemos, por ahora, exportar como artefacto. Pero esto no rompería el pipeline si fuera encontrada alguna vulnerabilidad. El job estaría generando el informe y nada más. Para que la salida del comando npm audit fuerce un error (exit 1) es necesario pasar el level, para caso encuentre un problema arriba de ese level fuerce la salida diferente de cero (éxito).
default:
tags:
- general
stages:
- build
- test
build:
stage: build
image: node:22-slim
script:
- node --version
- npm --version
- npm ci
- npm run build
artifacts:
when: on_success
expire_in: "1 hour"
paths:
- build/
unit-test:
stage: test # mismo stage
image: node:22-slim
script:
- npm ci
- npm test
lint-test:
stage: test # mismo stage
image: node:22-slim
script:
- npm ci
- npm run lint
vulnerability-test:
stage: test # mismo stage
image: node:22-slim
script:
- npm ci
- npm audit --audit-level=high --json > vulnerability-report.json
artifacts:
when: on_failure # Solo generará el informe en caso de fallo.
expire_in: "1 hour"
paths:
- vulnerability-report.json
Ahora tenemos 3 en ejecución en paralelo.

Este es el tipo de caso perfecto para trabajar con report. Herramientas que generan informes podemos trabajar de esta manera, pero es mucho más elegante trabajar con los reports poniendo cada cosa en su debido lugar.
artifacts:
when: on_failure # Solo generará el informe en caso de fallo.
expire_in: "1 hour"
paths:
- vulnerability-report.json