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).

Si tienes interés en profundizar más sobre este contenido, aquí puedes encontrar más detalles.

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:

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:

       "test": {
         "builder": "@angular-devkit/build-angular:jest",
         "options": {
           "tsConfig": "tsconfig.spec.json",
           "polyfills": ["zone.js", "zone.js/testing"]
         }
       }

Además, habría que instalar estas dos librerías:

npm install jest jest-environment-jsdom --save-dev

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:

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:

npm uninstall karma karma-chrome-launcher karma-coverage karma-jasmine karma-jasmine-html-reporter jasmine-core @types/jasmine

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:

       "test": {
         "builder": "@angular-devkit/build-angular:karma",
         "options": {
           "polyfills": [
             "zone.js",
             "zone.js/testing"
           ],
           "tsConfig": "tsconfig.spec.json",
           "inlineStyleLanguage": "scss",
           "assets": [
             "src/favicon.ico",
             "src/assets"
           ],
           "styles": [
             "src/styles.scss"
           ],
           "scripts": []
         }
       }

Después instalaremos las dependencias necesarias de Jest:

npm install jest jest-preset-angular @types/jest --save-dev

También actualizaremos los scripts de nuestro package.json. Para ello, cambiaremos esta línea:

 "scripts": {
   ...,
   "test": "ng test"
   ...
 },

Por estas otras dos:

 "scripts": {
   ...,
   "test": "jest",
   "coverage": "jest --coverage",
   ...
 },

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:

module.exports = {
 preset: "jest-preset-angular",
 roots: ["src"],
 setupFilesAfterEnv: ["<rootDir>/src/setup-jest.ts"],
 moduleNameMapper: {
   "@app/(.*)": "<rootDir>/src/app/$1",
   "@assets/(.*)": "<rootDir>/src/assets/$1",
   "@core/(.*)": "<rootDir>/src/app/core/$1",
   "@env": "<rootDir>/src/environments/environment",
   "@src/(.*)": "<rootDir>/src/src/$1",
   "@services/(.*)": "<rootDir>/src/app/core/services/$1",
   "@helpers/(.*)": "<rootDir>/src/app/helpers/$1",
   "@shared/(.*)": "<rootDir>/src/app/shared/$1",
 },
 coverageDirectory: "./coverage",
 collectCoverageFrom: [
   "src/app/**/*.ts",
   "!<rootDir>/node_modules/",
   "!<rootDir>/test/",
 ],
};

Este archivo de configuración especifica varios aspectos importantes:

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('Grupo de pruebas', () => {
 // Aquí van los tests relacionados
});
describe('Grupo de pruebas', () => {
// Ejemplo con it
 it('Debería sumar dos números', () => {
  // Lógica del test
 });
});

// Ejemplo con test
describe('Grupo de pruebas', () => {
 test('Debería sumar dos números', () => {
  // Lógica del test
 });
});
describe('Grupo de pruebas', () => {
 beforeEach(() => {
   // Lógica para configurar el estado inicial antes de cada test
 });
 // Aquí van los tests relacionados
});
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.

import { Component, OnInit } from '@angular/core';

import { SampleService } from '../../../core/services/sample-service/sample.service';

@Component({
 selector: 'app-home,
 template: `
   <div>
     <h1>{{ title }}</h1>
     <button class="btn btn--primary" (click)="increment()">
       Incrementar
     </button>
     Contador: {{ count }}
     <button class="btn btn--secondary" (click)="toggleVisibility()">
       Cambiar visibilidad
     </button>
     <p *ngIf="isVisible">Visible
     <div>Datos: {{ data }}</div>
   </div>
 `,
})
export class HomeComponent implements OnInit {
 title = 'Home component';
 count = 0;
 isVisible = false;
 data = '';

 constructor(private sampleService: SampleService) {}

 ngOnInit(): void {
   this.data = this.sampleService.getData();
 }

 increment(): void {
   this.count++;
 }

 toggleVisibility(): void {
   this.isVisible = !this.isVisible;
 }
}

El siguiente componente está compuesto por:

import { Injectable } from '@angular/core';

@Injectable({
 providedIn: 'root'
})
export class SampleService {
 getData(): string {
   return "Datos del servicio";
 }
}

El servicio cuenta con un método que devuelve una cadena de tipo string.

Empecemos a testear nuestro componente:

Configuración inicial con beforeEach:

 beforeEach(async () => {
   await TestBed.configureTestingModule({
     imports: [HttpClientTestingModule, HomeModule],
     providers: [SampleService],
   }).compileComponents();

   fixture = TestBed.createComponent(HomeComponent);
   component = fixture.componentInstance;
   service = TestBed.inject(SampleService);
   fixture.detectChanges();
 });

Explicación:

Test de creación del componente:

 it('should create the component', () => {
   expect(component).toBeTruthy();
 });

Explicació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:

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:

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:

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.

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable({
 providedIn: 'root'
})
export class PokemonService {
 private baseUrl = 'https://pokeapi.co/api/v2';

 constructor(private http: HttpClient) { }

 getPokemon(name: string): Observable<any> {
   return this.http.get(`${this.baseUrl}/pokemon/${name}`);
 }
}

Empecemos a testear nuestro servicio:

Configuración del entorno de pruebas:

 beforeEach(() => {
   TestBed.configureTestingModule({
     imports: [HttpClientTestingModule],
     providers: [PokemonService],
   });
   service = TestBed.inject(PokemonService);
   httpMock = TestBed.inject(HttpTestingController);
 });

Explicación:

Limpieza después de cada test:

 afterEach(() => {
   httpMock.verify();
 });

Explicación:

Test de creación del servicio:

 it('should be created', () => {
   expect(service).toBeTruthy();
 });

Explicación:

Test de llamada a la API con el servicio:

 it('should return a Pokemon data', () => {
   const mockPokemon = { name: 'pikachu', species: { name: 'pikachu' } };

   service.getPokemon('pikachu').subscribe((pokemon) => {
     expect(pokemon.name).toEqual('pikachu');
     expect(pokemon.species.name).toEqual('pikachu');
   });

   const req = httpMock.expectOne(
     'https://pokeapi.co/api/v2/pokemon/pikachu'
   );
   expect(req.request.method).toBe('GET');
   req.flush(mockPokemon);
 });

Explicación:

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.

Informe de cobertura con todos los archivos

Te dejamos aquí el código completo de este ejemplo.

Mejores prácticas y consejos

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.

Cuéntanos qué te parece.

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.

Suscríbete

Estamos comprometidos.

Tecnología, personas e impacto positivo.