En el anterior post vimos qué es GitHub Actions y cómo, a partir de templates, podemos crear flujos predeterminados que nos pueden servir para inicializar un proyecto con un flujo de CI rápidamente.
En este post vamos a hablar de cómo personalizar los workflows para adaptarlo a las necesidades de nuestro proyecto. Así mejoraremos la calidad del testing en nuestro flujo de integración continua.
Mejorando nuestro workflow de CI
Agregar los test E2E
Si hemos seguido los pasos anteriores deberíamos tener ya nuestra plantilla base. Si no los tenemos, en esta rama están subidos los cambios del proyecto para continuar a partir de este punto. Tened en cuenta que el fichero de workflow está subido para que se lancen los cambios contra la rama master, por lo que, si nos clonamos el proyecto desde esta rama, no se lanzará el flujo de CI hasta que lo integremos con nuestro master.
Como hemos visto a partir de la template, con unos pocos cambios ya tenemos un flujo funcionando. Además, hemos identificado otras funcionalidades en nuestro flujo de CI que nos gustaría agregar para mejorar la calidad del proyecto, que son las siguientes:
- Aparte de los tests unitarios, incluir lanzar los tests e2e que tenemos con Cypress en al menos las últimas versiones de Chrome y Firefox.
- Generar un fichero de reporte de las ejecuciones e2e y unitarias que podamos descargar cuando finalice la ejecución.
- Agregar las validaciones del linter al flujo.
- Paralelizar la ejecución de los tests unitarios, e2e y linter para optimizar el tiempo de ejecución de nuestro flujo.
Empezamos por agregar los e2e y generar el reporte de la ejecución. Primero, para lanzar los tests e2e en Firefox y Chrome, tenemos que modificar nuestro fichero package.json. Cambiamos la línea existente al script test:e2e:ci por las dos siguientes:
"test:e2e:ci:chrome": "start-server-and-test preview http://127.0.0.1:5050/ 'cypress run --quiet --record false --browser chrome --reporter junit --reporter-options mochaFile=e2e-chrome-test-result.xml,toConsole=true'",
"test:e2e:ci:firefox": "start-server-and-test preview http://127.0.0.1:5050/ 'cypress run --quiet --record false --browser firefox --reporter junit --reporter-options mochaFile=e2e-firefox-test-result.xml'",
Modificamos también el script test:unit:ci para incluir el reporte:
"test:unit:ci": "cypress run-ct --quiet --reporter junit --reporter-options mochaFile=unit-test-result.xml,toConsole=true",
Incluimos en nuestro fichero .github/workflows/node.js.yml los siguientes cambios para lanzar los tests e2e, linter, y el Action, que nos va a permitir subir los ficheros de los reportes:
- run: npm ci
- run: npm run lint
- run: npm run build --if-present
- run: npm run test:unit:ci
- run: npm run test:e2e:ci:chrome
- run: npm run test:e2e:ci:firefox
- uses: actions/upload-artifact@v3
with:
name: tests-results-${{ matrix.node-version }}
path: |
*test-result.xml
Subimos los cambios al repositorio:
git add package.json
git add .github/workflows/node.js.yml
git commit -m 'add e2e cypress and linter execution in CI workflow'
git push
Y vemos como ahora, al lanzar los 3 jobs, se generan 3 artefactos .zip que contienen los ficheros con los resultados de cada ejecución de los tests unitarios y e2e:
Paralelización de los jobs
Por último, nos queda paralelizar la ejecución de los tests de nuestra CI para reducir el tiempo de ejecución. Aunque con nuestro minuto y medio actual somos la envidia de cualquier proyecto, vamos a hacerlo para ver cómo podemos combinar la utilización de múltiples jobs y matrices.
Antes de poder paralelizar las ejecuciones debemos tener en cuenta varios puntos, ya que tenemos que aplicar varios cambios en nuestro workflow. Ahora mismo solo tenemos un job llamado build con múltiples steps que engloban todo nuestro flujo de CI. Esto hace que todos nuestros steps se ejecutan 3 veces en diferentes jobs por las 3 versiones de node que tenemos indicadas en nuestro campo matrix. Es decir, instalamos 3 veces las dependencias del proyecto, lanzamos los tests unitarios 3 veces, etc.
Entonces, para realizar el cambio, tenemos que sacar los steps que, ahora mismo, lanzan los comandos de tests unitarios, linter y e2e a diferentes jobs para que se puedan lanzar de manera paralela. Sin embargo, esto implica las siguientes consideraciones:
- Por cómo funcionan los jobs de GitHub Actions, la estructura de ficheros en cada Job es independiente. Es decir, si en un job hemos instalado las dependencias del proyecto, en los demás tenemos que volver a repetir los steps para instalarlas o agregar nuevos para almacenarlas en caché y recuperarlas.
- Generamos dependencias entre jobs. Los tests e2e del proyecto que tenemos necesitan que se haya generado correctamente una build y tener acceso a la carpeta dist con los ficheros generados, por lo que tenemos que agregar steps adicionales para almacenar el artefacto de la build. Y, una vez finalizado el job de la build, recuperarlo para los tests e2e.
- Antes disponíamos de un único artefacto, que era un zip que contenía todos los diferentes reportes que se generaban al final de ejecutar todas las pruebas. Ahora, al paralelizar, tenemos que generar un artefacto diferente por cada fichero.
Vamos a empezar creando los jobs y steps del linter y tests unitarios, que no tienen dependencia de generar la build:
ci-linter:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Use Node.js 16.x
uses: actions/setup-node@v3
with:
node-version: 16.x
cache: 'npm'
- run: npm ci
- run: npm run lint
ci-unit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Use Node.js 16.x
uses: actions/setup-node@v3
with:
node-version: 16.x
cache: 'npm'
- run: npm ci
- run: npm run test:unit:ci
- uses: actions/upload-artifact@v3
with:
name: tests-results-unit
path: |
*test-result.xml
Como podemos ver, tenemos que volver a indicar en cada job los pasos para instalar las dependencias, pero, como las estamos recuperando de la caché, no debería afectar mucho al rendimiento. Por otro lado, para simplificar sólo lanzamos los tests con la versión de node 16.x.
Ahora simplificamos el job build que teníamos para incluir el step con las actions para almacenar la build generada y utilizarla después:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [12.x, 14.x, 16.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm ci
- run: npm run build
- uses: actions/upload-artifact@v3
with:
name: build-folder
path: |
dist
Por último, agregamos los jobs para incluir los tests e2e. Utilizaremos estos actions para descargar la build creada anteriormente:
ci-e2e-firefox:
needs: [build]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Use Node.js 16.x
uses: actions/setup-node@v3
with:
node-version: 16.x
cache: 'npm'
- uses: actions/download-artifact@v3
with:
name: build-folder
path: ./dist
- run: npm ci
- run: npm run test:e2e:ci:firefox
- uses: actions/upload-artifact@v3
with:
name: tests-results-firefox-e2e
path: |
*test-result.xml
ci-e2e-chrome:
needs: [build]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Use Node.js 16.x
uses: actions/setup-node@v3
with:
node-version: 16.x
cache: 'npm'
- uses: actions/download-artifact@v3
with:
name: build-folder
path: ./dist
- run: npm ci
- run: npm run test:e2e:ci:chrome
- uses: actions/upload-artifact@v3
with:
name: tests-results-chrome-e2e
path: |
*test-result.xml
Como podemos comprobar, hemos incluido también needs: [build] para indicar que nuestros jobs de los e2e de Chrome y Firefox solo se lanzarán cuando haya finalizado el job build.
Después de estos cambios el fichero entero se tendría que parecer a lo siguiente:
# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: Node.js CI
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [12.x, 14.x, 16.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm ci
- run: npm run build
- uses: actions/upload-artifact@v3
with:
name: build-folder
path: |
dist
ci-linter:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Use Node.js 16.x
uses: actions/setup-node@v3
with:
node-version: 16.x
cache: 'npm'
- run: npm ci
- run: npm run lint
ci-unit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Use Node.js 16.x
uses: actions/setup-node@v3
with:
node-version: 16.x
cache: 'npm'
- run: npm ci
- run: npm run test:unit:ci
- uses: actions/upload-artifact@v3
with:
name: tests-results-unit
path: |
*test-result.xml
ci-e2e-firefox:
needs: [build]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Use Node.js 16.x
uses: actions/setup-node@v3
with:
node-version: 16.x
cache: 'npm'
- uses: actions/download-artifact@v3
with:
name: build-folder
path: ./dist
- run: npm ci
- run: npm run test:e2e:ci:firefox
- uses: actions/upload-artifact@v3
with:
name: tests-results-firefox-e2e
path: |
*test-result.xml
ci-e2e-chrome:
needs: [build]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Use Node.js 16.x
uses: actions/setup-node@v3
with:
node-version: 16.x
cache: 'npm'
- uses: actions/download-artifact@v3
with:
name: build-folder
path: ./dist
- run: npm ci
- run: npm run test:e2e:ci:chrome
- uses: actions/upload-artifact@v3
with:
name: tests-results-chrome-e2e
path: |
*test-result.xml
Ahora subimos los cambios con los siguientes comandos:
git add .github/
git commit -m 'parallel ci test steps'
git push
Y, por último, deberíamos poder visualizar este resultado cuando finaliza la ejecución:
Hemos reducido nuestro flujo de CI a 50 segundos, ¡no está nada mal!
Lanzar diferentes pruebas E2E con una matrix
Como bonus para rematar la euforia de la robusta CI que estamos montando y para seguir mostrando lo que podemos realizar con GitHub Actions, vamos a mejorar nuestros tests e2e: incluiremos una matrix para lanzar simultáneamente versiones de diferentes navegadores. Para realizar estos cambios, por suerte para nosotros, Cypress tiene creadas unas imágenes docker con diferentes versiones que podemos utilizar en nuestro workflow.
Empecemos por nuestro job que lanza los tests para Firefox. Con los cambios quedaría de la siguiente manera:
ci-e2e-firefox:
needs: [build]
runs-on: ubuntu-latest
container: ${{ matrix.cypress_container.name }}
strategy:
matrix:
include:
- cypress_container: "cypress/browsers:node14.17.0-chrome91-ff89"
firefox_version: 89.0.2
- cypress_container: "cypress/browsers:node16.5.0-chrome94-ff93"
firefox_version: 93.0
steps:
- uses: actions/checkout@v3
- name: Use Node.js 16.x
uses: actions/setup-node@v3
with:
node-version: 16.x
cache: 'npm'
- uses: actions/download-artifact@v3
with:
name: build-folder
path: ./dist
- run: npm ci
- run: npm run test:e2e:ci:firefox
- uses: actions/upload-artifact@v3
with:
name: tests-results-firefox-${{ matrix.firefox_version }}-e2e
path: |
*test-result.xml
Hemos agregado el campo container. Indica el contenedor de docker que queremos lanzar. Como queremos lanzar múltiples contenedores para probar diferentes versiones de Firefox, el valor del campo container lo recupera del campo cypress_container donde incluimos las versiones del contenedor que queramos lanzar en paralelo. Junto a este atributo hemos agregado también el campo firefox_version, para indicar la versión de Firefox incluida en cada versión del contenedor y usar este valor para generar el artefacto de los resultados de los tests. Si no ponemos un valor único para cada reporte que vayamos a generar solo tendremos el del último job que se ejecute.
Ahora para el job de los tests e2e de Chrome es la misma estructura. Simplemente cambiamos el valor de la versión de Firefox por la de Chrome, que incluyen los mismos contenedores:
ci-e2e-chrome:
needs: [build]
runs-on: ubuntu-latest
container: ${{ matrix.cypress_container }}
strategy:
matrix:
include:
- cypress_container: "cypress/browsers:node14.17.0-chrome91-ff89"
chrome_version: 91.0.4472.114
- cypress_container: "cypress/browsers:node16.5.0-chrome94-ff93"
chrome_version: 94.0.4606.71
steps:
- uses: actions/checkout@v3
- name: Use Node.js 16.x
uses: actions/setup-node@v3
with:
node-version: 16.x
cache: 'npm'
- uses: actions/download-artifact@v3
with:
name: build-folder
path: ./dist
- run: npm ci
- run: npm run test:e2e:ci:chrome
- uses: actions/upload-artifact@v3
with:
name: tests-results-chrome-${{ matrix.chrome_version }}-e2e
path: |
*test-result.xml
Ahora subimos los cambios:
git add .github/
git commit -m 'run e2e in multiple chrome and firefox browsers versions'
git push
Y comprobamos el resultado:
Como podemos ver, parece que al incluir la ejecución en contenedores de los tests se ha ralentizado, aunque esto también puede deberse a cómo están construidas las imágenes en las que se lanzan estos tests. Pese a ello, seguimos teniendo un flujo de CI bastante completo en solo dos minutos y medio.
Conclusión
Con esto terminamos de ver el flujo de CI. En esta rama podéis encontrar el código de los cambios introducidos en este post.
En resumen, hemos agregado nuestros test e2e al flujo, utilizando una matrix y contenedores con docker para lanzar en paralelo versiones con diferentes navegadores. Y, también, cómo generar dependencias entre jobs para que solo se ejecuten según el estado en el que han finalizado otros jobs.
En el próximo y último post definiremos nuestro flujo de CD para desplegar nuestra aplicación una vez finalicen correctamente nuestras validaciones de CI que hemos creado.


