Descubre cómo el uso de Jest puede mejorar la eficiencia, mantenibilidad y robustez de tus aplicaciones en Angular.
¿Qué es Jest y por qué es esencial para el testing en Angular?
Históricamente, el equipo de Angular ha preferido utilizar Jasmine como framework para las pruebas unitarias y Karma como el test runner para ejecutarlas. Recientemente, desde el equipo de Angular se ha hecho una encuesta para ver la satisfacción de la Comunidad respecto al framework.
El resultado en cuanto a testing no ha sido nada positivo, ya que ha quedado evidenciada la baja satisfacción de los desarrolladores/as debido, principalmente, a la lentitud a la hora de ejecutar los tests y la dificultad para integrarlos en entornos de integración continua (CI/CD).
En este punto es donde entra en juego Jest. Este framework de testing es popular por su simplicidad y eficacia, ofreciendo las siguientes características que lo hacen destacar en el ámbito del testing unitario:
Configuración simplificada. Jest viene con una configuración predeterminada válida para la mayoría de los casos, lo que facilita su adopción y uso sin necesidad de ajustes complicados.
Soporte integrado para mocking y spies. Ofrece herramientas de mocking y spies integradas, lo que elimina la necesidad de bibliotecas externas para estas funcionalidades.
Rendimiento mejorado. Jest ejecuta pruebas en paralelo, lo que puede reducir significativamente el tiempo de ejecución de las pruebas, especialmente en proyectos grandes.
Informe de cobertura de código. Incluye herramientas de cobertura de código listas para usar, lo que sirve de gran ayuda al desarrollador/a, ya que garantiza que todas las partes del código están adecuadamente probadas.
Ambiente de pruebas consistente. Utiliza JSDOM para simular el entorno del DOM, permitiendo pruebas más consistentes y controladas sin depender de un navegador real.
Migración y actualización continuas. Jest es mantenido activamente, añadiendo nuevas características y mejoras de manera regular, lo que asegura que las herramientas de pruebas se mantengan modernas, eficientes y sin vulnerabilidades.
Instalación Jest
Hasta la llegada de Angular 16, no había una forma nativa ofrecida por Angular que diese soporte al uso de Jest. Sin embargo, como se menciona en el post que anteriormente compartimos, ahora se ha habilitado esta opción de manera experimental.
Para ello, habría que modificar esta parte en el archivo angular.json:
Sin embargo, al pasar los test nos avisaría de que es una característica que está en modo experimental y que no está preparada para un uso productivo:
NOTE: The Jest builder is currently EXPERIMENTAL and not ready for production use.
Es por ello que, hasta que no esté completamente desarrollado por Angular, vamos a tener que instalar Jest de manera manual hasta que Angular lo lance de manera oficial:
Contaremos con un entorno de desarrollo configurado con las siguientes características:
Versión de Angular CLI: 17.3.6
Node: 18.18.0
Gestor de paquetes: npm 9.8.1
Sistema Operativo: darwin arm64
Lo primero que haremos será crear un proyecto nuevo, para ello introduciremos en una terminal:
ng new project-test-jest --standalone=false
A continuación, tendremos que desinstalar las librerías de nuestro proyecto que hagan referencia a Karma y Jasmine, en nuestro caso:
Posteriormente, eliminaremos del archivo angular.json la sección que configura los tests. Esto se debe a que no utilizaremos la CLI de Angular ni su configuración predeterminada para ejecutar los tests:
Adicionalmente, será necesario añadir los tipos de Jest, reemplazando los de Jasmine. Para ello, en el archivo tsconfig.spec.json, cambiaremos los types de Jasmine por los de Jest.
"types":["jest"]
Para establecer Jest como el framework de pruebas en nuestro proyecto Angular, debemos crear un archivo de configuración de Jest llamado jest.config.js en la raíz de nuestro proyecto. Este archivo configurará Jest para trabajar adecuadamente con Angular. A continuación, se muestra un ejemplo de cómo debería ser el contenido de este archivo:
Este archivo de configuración especifica varios aspectos importantes:
preset: utiliza el preset de jest-preset-angular para configuraciones predefinidas propias de Angular.
roots: define la carpeta raíz de los archivos de test.
setupFilesAfterEnv: indica la ruta del archivo de configuración específico que se ejecutará después de que Jest haya sido inicializado.
moduleNameMapper: crea alias a rutas específicas para simplificar los imports en los tests y conseguir que sean más legibles.
coverageDirectory: indica la carpeta donde se almacenarán los informes de cobertura de código.
collectCoverageFrom: indica aquellos archivos de los que se recogerá la cobertura de código, excluyendo algunos directorios como node_modules y cualquier carpeta relacionada con los tests.
Para configurar Jest en nuestro proyecto, también debemos crear un archivo de configuración. Para ello, creamos un archivo llamado src/setup-jest.ts e incluimos la siguiente línea de importación:
import'jest-preset-angular/setup-jest';
Esta importación es necesaria para configurar y preparar el entorno de Jest específicamente para proyectos Angular, asegurando que todas las funcionalidades necesarias de Jest y Angular estén disponibles y optimizadas para los tests.
Estructura básica de un test con Jest
Jest utiliza funciones como describe, it (o test), beforeEach y afterEach para estructurar y organizar los tests. A continuación, vamos a ver la sintaxis de estas funciones:
describe: esta función se utiliza para agrupar un conjunto de tests relacionados y recibe dos argumentos: una cadena de texto que describe el grupo de pruebas y una función que contiene los tests.
describe('Grupo de pruebas',()=>{// Aquí van los tests relacionados});
it / test: ambas funciones se utilizan para definir un test individual. it y test son equivalentes y se pueden usar indistintamente. Ambas toman dos argumentos: una cadena de texto que describe el test y una función que contiene la lógica del test.
describe('Grupo de pruebas',()=>{// Ejemplo con itit('Debería sumar dos números',()=>{// Lógica del test});});// Ejemplo con testdescribe('Grupo de pruebas',()=>{test('Debería sumar dos números',()=>{// Lógica del test});});
beforeEach: esta función se ejecuta antes de cada test dentro de un bloque describe. Se utiliza para establecer el estado inicial o configurar las dependencias antes de ejecutar cada test. beforeEach toma una función como argumento, que contiene la lógica a ejecutar antes de cada test.
describe('Grupo de pruebas',()=>{beforeEach(()=>{// Lógica para configurar el estado inicial antes de cada test});// Aquí van los tests relacionados});
afterEach: esta función se ejecuta después de cada test dentro de un bloque describe. Se utiliza para realizar la limpieza, como eliminar objetos creados, restablecer mocks o liberar recursos después de cada test. afterEach también toma una función como argumento, que contiene la lógica a ejecutar después de cada test.
describe('Grupo de pruebas',()=>{afterEach(()=>{// Lógica para limpiar después de cada test});// Aquí van los tests relacionados});
Testing de componentes en Angular
A continuación, veremos un ejemplo de cómo testear las diferentes partes de un componente. Para ello, vamos a partir de un componente llamado HomeComponent y de un servicio llamado SampleService.
TestBed: se configura el entorno de pruebas para el componente HomeComponent. Se declaran los componentes que se probarán y los servicios que se inyectarán.
compileComponents(): compila los componentes HTML y CSS, preparando el ambiente para ejecutar los tests.
createComponent(): crea una instancia del componente HomeComponent, que será el sujeto de las pruebas.
detectChanges(): aplica enlaces de datos y ejecuta la detección inicial de cambios, que es crucial para inicializar propiedades y ejecutar lógicas como ngOnInit.
Test de creación del componente:
it('should create the component',()=>{expect(component).toBeTruthy();});
Explicación:
Este test verifica que el componente se haya creado correctamente. La expectativa es que el componente sea una instancia válida, lo que implica que no hay errores inmediatos en su construcción o inicialización.
Test de interacción del usuario: click en botón de incremento:
it('should update the count property when increment button is clicked',()=>{const incrementButton = fixture.debugElement.query(By.css('.btn--primary'));
incrementButton.triggerEventHandler('click',null);
fixture.detectChanges();expect(component.count).toBe(1);});
Explicación:
query() y By.css: se selecciona el botón de incremento utilizando su clase CSS. Esto permite simular eventos específicos en elementos específicos de la UI.
triggerEventHandler(): simula un click en el botón de incremento.
expect(): verifica que la propiedad count del componente haya incrementado a 1 como resultado del click, lo que valida la lógica de incremento del componente.
Test de interacción del usuario: click en botón de visibilidad:
it('should toggle the boolean property when toggle visibility button is clicked',(){const toggleVisibilityButton = fixture.debugElement.query(
By.css('.btn--secondary'));expect(component.isVisible).toBe(false);
toggleVisibilityButton.triggerEventHandler('click',null);
fixture.detectChanges();expect(component.isVisible).toBe(true);
toggleVisibilityButton.triggerEventHandler('click',null);
fixture.detectChanges();expect(component.isVisible).toBe(false);});
Explicación:
Similar al test anterior, pero en este caso se verifica la funcionalidad del botón que alterna la visibilidad de un elemento en la UI.
Se verifica que la propiedad isVisible cambia correctamente cada vez que se hace click en el botón, lo que demuestra la reactividad del componente a las interacciones del usuario.
Test de interacción con el servicio:
it('should call getData() method from SampleService on component init',()=>{
fixture = TestBed.createComponent(HomeComponent);const spy = jest
.spyOn(service,'getData').mockReturnValue('Datos del servicio');
fixture.detectChanges();expect(spy).toHaveBeenCalled();expect(component.data).toBe('Datos del servicio');});
Explicación:
jest.spyOn(): espía el método getData() del servicio SampleService.
mockReturnValue(): configura un valor de retorno simulado para cuando se llame al método espiado.
toHaveBeenCalled(): verifica que el método espiado se haya llamado durante la inicialización del componente, lo que indica que el componente depende directamente del servicio para obtener datos.
Este test también asegura que los datos obtenidos del servicio se asignen correctamente a la propiedad data del componente.
Testing de servicios en Angular
A continuación, veremos un ejemplo de cómo testear un servicio que hace una llamada a una API real, en este caso vamos a utilizar la PokeApi. Para ello, vamos a partir de un servicio llamado PokemonService.
beforeEach(()=>{
TestBed.configureTestingModule({
imports:[HttpClientTestingModule],
providers:[PokemonService],});
service = TestBed.inject(PokemonService);
httpMock = TestBed.inject(HttpTestingController);});
Explicación:
HttpClientTestingModule: proporciona un entorno simulado para las llamadas HTTP, permitiendo que las pruebas no dependan de llamadas a servicios externos reales.
PokemonService: es el servicio que estamos probando, el cual hace llamadas HTTP para obtener datos de la PokeAPI.
HttpTestingController: se usa para capturar y manejar las solicitudes HTTP, permitiendo verificar que se realizan las solicitudes correctas y responder a ellas con datos simulados.
Limpieza después de cada test:
afterEach(()=>{
httpMock.verify();});
Explicación:
verify(): limpia y verifica que no haya solicitudes pendientes en HttpTestingController, lo cual es importante para asegurar que los tests no interfieran unos con otros.
Test de creación del servicio:
it('should be created',()=>{expect(service).toBeTruthy();});
Explicación:
Verifica que se haya creado una instancia del servicio correctamente, lo cual es una comprobación básica de que la inyección de dependencias y la configuración del módulo son correctas.
Suscripción al servicio: se suscribe a getPokemon() y se espera recibir datos que coincidan con el mockPokemon definido.
Interceptación y manejo de la solicitud HTTP: expectOne asegura que se hace una solicitud HTTP GET a la URL esperada y flush envía una respuesta simulada al suscriptor.
Verificaciones: se comprueba que la solicitud sea un GET y que los datos recibidos coincidan con los datos simulados.
Informe de cobertura
Para facilitar los tests y comprender qué funcionalidades han sido testeadas, es necesario utilizar el informe de cobertura generado por Jest. Durante la configuración de nuestro entorno hemos incorporado un script para ejecutar Jest con el flag para la creación de este informe.
Para generar el informe, ejecuta el siguiente comando en la terminal:
npm run coverage
Una vez finalizados los tests, se creará automáticamente un directorio llamado coverage. Este contiene el informe de cobertura. Para visualizar el informe en formato HTML, tendremos que abrir el archivo coverage/lcov-report/index.html en el navegador. Al hacerlo, podremos ver detalladamente el informe de cobertura generado.
Mantén los tests simples y enfocados: cada test debe probar una sola funcionalidad o comportamiento. Esto hace que los tests sean más fáciles de mantener y de entender.
Utiliza nombres descriptivos: los nombres de tus tests deben describir claramente lo que estás probando. Esto facilita la identificación de problemas si un test falla.
Asegúrate de que los tests sean independientes: cada test debe ser capaz de ejecutarse de forma independiente y en cualquier orden. No deben depender del resultado de otros tests.
Usa beforeEach y afterEach para la configuración y limpieza: estos métodos te permiten preparar el entorno de testing antes de cada test y limpiar cualquier recurso después de cada test, respectivamente.
Emplea mocks y spies para aislar el código: utiliza mocks para simular objetos y comportamientos externos, y spies para espiar y manipular llamadas a métodos. Esto te permite probar tu código de manera aislada y controlada.
Comprueba casos límite y excepciones: además de los casos típicos, asegúrate de probar todos los caminos y los casos límite.
Conclusión
Integrar Jest en tus proyectos Angular marca una diferencia considerable en la forma en que gestionamos las pruebas unitarias. Jest no sólo acelera el proceso de pruebas gracias a su ejecución paralela y configuración simplificada, sino que también mejora la calidad general del código.
Esto es vital, ya que las pruebas unitarias aseguran que cada componente de tu aplicación funcione correctamente de manera independiente, lo que es fundamental para mantener la integridad de tus aplicaciones.
Adoptar Jest podría no sólo facilitarte la vida como desarrollador/a, sino también contribuir a mejorar la estabilidad y mantenibilidad de tus aplicaciones.
“La calidad no es un acto, es un hábito”- Aristóteles.
Sergio Casado
Llevo desarrollando código desde el lado Front-end desde hace más de 5 años. JavaScript lover, independientemente del framework. Me apasiona todo lo que tiene que ver con tecnología e historia. En cuanto a mis aficiones, me encanta conocer nuevas culturas, Por supuesto, seriéfilo.
Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.
Cuéntanos qué te parece.