En este tema, aprenderemos cómo realizar pruebas unitarias en los servicios de Angular. Los servicios son una parte crucial de cualquier aplicación Angular, ya que encapsulan la lógica de negocio y la comunicación con APIs externas. Probar estos servicios asegura que nuestra lógica de negocio funcione correctamente y que nuestra aplicación sea robusta y mantenible.
Contenido
Introducción a las Pruebas de Servicios
Las pruebas de servicios en Angular se centran en verificar que los métodos de los servicios funcionen como se espera. Esto incluye:
- Verificar que los métodos devuelvan los valores correctos.
- Asegurarse de que las dependencias se manejen correctamente.
- Comprobar que las llamadas a APIs externas se realicen correctamente.
Configuración del Entorno de Pruebas
Angular CLI proporciona un entorno de pruebas listo para usar con Jasmine y Karma. Para comenzar a escribir pruebas, asegúrate de que tu proyecto esté configurado correctamente.
-
Instalación de Dependencias: Angular CLI ya incluye Jasmine y Karma, pero asegúrate de tener las siguientes dependencias en tu
package.json
:"devDependencies": { "@angular/cli": "latest", "@angular/core": "latest", "jasmine-core": "~3.6.0", "karma": "~5.0.0", "karma-chrome-launcher": "~3.1.0", "karma-coverage-istanbul-reporter": "~3.0.2", "karma-jasmine": "~3.3.0", "karma-jasmine-html-reporter": "^1.5.0" }
-
Configuración de Karma: Asegúrate de que el archivo
karma.conf.js
esté configurado correctamente. Este archivo se genera automáticamente cuando creas un proyecto Angular con Angular CLI.
Creación de un Servicio de Ejemplo
Vamos a crear un servicio simple que obtenga datos de una API ficticia.
-
Generar el Servicio:
ng generate service data
-
Implementar el Servicio:
// src/app/data.service.ts import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs'; @Injectable({ providedIn: 'root' }) export class DataService { private apiUrl = 'https://api.example.com/data'; constructor(private http: HttpClient) { } getData(): Observable<any> { return this.http.get<any>(this.apiUrl); } }
Escribiendo Pruebas Unitarias para Servicios
Ahora que tenemos nuestro servicio, escribamos algunas pruebas unitarias para verificar su funcionalidad.
- Configurar el Archivo de Pruebas:
// src/app/data.service.spec.ts import { TestBed } from '@angular/core/testing'; import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; import { DataService } from './data.service'; describe('DataService', () => { let service: DataService; let httpMock: HttpTestingController; beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], providers: [DataService] }); service = TestBed.inject(DataService); httpMock = TestBed.inject(HttpTestingController); }); afterEach(() => { httpMock.verify(); }); it('should be created', () => { expect(service).toBeTruthy(); }); it('should fetch data from API', () => { const dummyData = [{ id: 1, name: 'John' }, { id: 2, name: 'Doe' }]; service.getData().subscribe(data => { expect(data.length).toBe(2); expect(data).toEqual(dummyData); }); const req = httpMock.expectOne(service['apiUrl']); expect(req.request.method).toBe('GET'); req.flush(dummyData); }); });
Explicación del Código
- Configuración del Módulo de Pruebas: Utilizamos
TestBed
para configurar el entorno de pruebas. ImportamosHttpClientTestingModule
para simular las solicitudes HTTP. - Inyección de Dependencias: Utilizamos
TestBed.inject
para obtener instancias del servicio y del controlador de pruebas HTTP. - Verificación de Solicitudes HTTP: Utilizamos
HttpTestingController
para verificar que las solicitudes HTTP se realicen correctamente y para simular respuestas de la API.
Simulación de Dependencias
En algunos casos, los servicios pueden depender de otros servicios. En tales casos, podemos simular estas dependencias utilizando spyOn
o creando mocks personalizados.
Ejemplo de Simulación de Dependencias
Supongamos que nuestro DataService
depende de otro servicio llamado AuthService
.
-
Crear el Servicio de Autenticación:
// src/app/auth.service.ts import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root' }) export class AuthService { isAuthenticated(): boolean { return true; } }
-
Modificar el Servicio de Datos:
// src/app/data.service.ts import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs'; import { AuthService } from './auth.service'; @Injectable({ providedIn: 'root' }) export class DataService { private apiUrl = 'https://api.example.com/data'; constructor(private http: HttpClient, private authService: AuthService) { } getData(): Observable<any> { if (this.authService.isAuthenticated()) { return this.http.get<any>(this.apiUrl); } else { throw new Error('User not authenticated'); } } }
-
Escribir Pruebas con Simulación de Dependencias:
// src/app/data.service.spec.ts import { TestBed } from '@angular/core/testing'; import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; import { DataService } from './data.service'; import { AuthService } from './auth.service'; describe('DataService', () => { let service: DataService; let httpMock: HttpTestingController; let authServiceSpy: jasmine.SpyObj<AuthService>; beforeEach(() => { const spy = jasmine.createSpyObj('AuthService', ['isAuthenticated']); TestBed.configureTestingModule({ imports: [HttpClientTestingModule], providers: [ DataService, { provide: AuthService, useValue: spy } ] }); service = TestBed.inject(DataService); httpMock = TestBed.inject(HttpTestingController); authServiceSpy = TestBed.inject(AuthService) as jasmine.SpyObj<AuthService>; }); afterEach(() => { httpMock.verify(); }); it('should fetch data from API if authenticated', () => { const dummyData = [{ id: 1, name: 'John' }, { id: 2, name: 'Doe' }]; authServiceSpy.isAuthenticated.and.returnValue(true); service.getData().subscribe(data => { expect(data.length).toBe(2); expect(data).toEqual(dummyData); }); const req = httpMock.expectOne(service['apiUrl']); expect(req.request.method).toBe('GET'); req.flush(dummyData); }); it('should throw error if not authenticated', () => { authServiceSpy.isAuthenticated.and.returnValue(false); expect(() => service.getData()).toThrow(new Error('User not authenticated')); }); });
Explicación del Código
- Simulación de
AuthService
: Utilizamosjasmine.createSpyObj
para crear un mock delAuthService
y simular su métodoisAuthenticated
. - Inyección del Mock: Proveemos el mock del
AuthService
en el módulo de pruebas utilizando{ provide: AuthService, useValue: spy }
. - Pruebas Condicionales: Escribimos pruebas para verificar el comportamiento del
DataService
tanto cuando el usuario está autenticado como cuando no lo está.
Ejercicios Prácticos
- Ejercicio 1: Crea un servicio que obtenga una lista de usuarios de una API ficticia y escribe pruebas unitarias para verificar que el servicio funcione correctamente.
- Ejercicio 2: Modifica el servicio anterior para que dependa de un servicio de autenticación y escribe pruebas unitarias para verificar el comportamiento del servicio en función del estado de autenticación.
Soluciones
Ejercicio 1
-
Crear el Servicio de Usuarios:
// src/app/user.service.ts import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs'; @Injectable({ providedIn: 'root' }) export class UserService { private apiUrl = 'https://api.example.com/users'; constructor(private http: HttpClient) { } getUsers(): Observable<any> { return this.http.get<any>(this.apiUrl); } }
-
Escribir Pruebas para el Servicio de Usuarios:
// src/app/user.service.spec.ts import { TestBed } from '@angular/core/testing'; import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; import { UserService } from './user.service'; describe('UserService', () => { let service: UserService; let httpMock: HttpTestingController; beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], providers: [UserService] }); service = TestBed.inject(UserService); httpMock = TestBed.inject(HttpTestingController); }); afterEach(() => { httpMock.verify(); }); it('should be created', () => { expect(service).toBeTruthy(); }); it('should fetch users from API', () => { const dummyUsers = [{ id: 1, name: 'John' }, { id: 2, name: 'Doe' }]; service.getUsers().subscribe(users => { expect(users.length).toBe(2); expect(users).toEqual(dummyUsers); }); const req = httpMock.expectOne(service['apiUrl']); expect(req.request.method).toBe('GET'); req.flush(dummyUsers); }); });
Ejercicio 2
-
Modificar el Servicio de Usuarios:
// src/app/user.service.ts import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs'; import { AuthService } from './auth.service'; @Injectable({ providedIn: 'root' }) export class UserService { private apiUrl = 'https://api.example.com/users'; constructor(private http: HttpClient, private authService: AuthService) { } getUsers(): Observable<any> { if (this.authService.isAuthenticated()) { return this.http.get<any>(this.apiUrl); } else { throw new Error('User not authenticated'); } } }
-
Escribir Pruebas con Simulación de Dependencias:
// src/app/user.service.spec.ts import { TestBed } from '@angular/core/testing'; import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; import { UserService } from './user.service'; import { AuthService } from './auth.service'; describe('UserService', () => { let service: UserService; let httpMock: HttpTestingController; let authServiceSpy: jasmine.SpyObj<AuthService>; beforeEach(() => { const spy = jasmine.createSpyObj('AuthService', ['isAuthenticated']); TestBed.configureTestingModule({ imports: [HttpClientTestingModule], providers: [ UserService, { provide: AuthService, useValue: spy } ] }); service = TestBed.inject(UserService); httpMock = TestBed.inject(HttpTestingController); authServiceSpy = TestBed.inject(AuthService) as jasmine.SpyObj<AuthService>; }); afterEach(() => { httpMock.verify(); }); it('should fetch users from API if authenticated', () => { const dummyUsers = [{ id: 1, name: 'John' }, { id: 2, name: 'Doe' }]; authServiceSpy.isAuthenticated.and.returnValue(true); service.getUsers().subscribe(users => { expect(users.length).toBe(2); expect(users).toEqual(dummyUsers); }); const req = httpMock.expectOne(service['apiUrl']); expect(req.request.method).toBe('GET'); req.flush(dummyUsers); }); it('should throw error if not authenticated', () => { authServiceSpy.isAuthenticated.and.returnValue(false); expect(() => service.getUsers()).toThrow(new Error('User not authenticated')); }); });
Conclusión
En esta sección, hemos aprendido cómo escribir pruebas unitarias para servicios en Angular. Hemos cubierto la configuración del entorno de pruebas, la creación de un servicio de ejemplo, la escritura de pruebas unitarias y la simulación de dependencias. Las pruebas unitarias son esenciales para asegurar la calidad y la robustez de nuestras aplicaciones Angular. En el próximo tema, exploraremos las pruebas de extremo a extremo para verificar el comportamiento completo de nuestra aplicación.
Curso de Angular
Módulo 1: Introducción a Angular
- ¿Qué es Angular?
- Configuración del Entorno de Desarrollo
- Arquitectura de Angular
- Primera Aplicación Angular
Módulo 2: Componentes de Angular
- Entendiendo los Componentes
- Creación de Componentes
- Plantillas de Componentes
- Estilos de Componentes
- Interacción entre Componentes
Módulo 3: Enlace de Datos y Directivas
- Interpolación y Enlace de Propiedades
- Enlace de Eventos
- Enlace de Datos Bidireccional
- Directivas Incorporadas
- Directivas Personalizadas
Módulo 4: Servicios e Inyección de Dependencias
- Introducción a los Servicios
- Creación y Uso de Servicios
- Inyección de Dependencias
- Inyectores Jerárquicos
Módulo 5: Enrutamiento y Navegación
Módulo 6: Formularios en Angular
- Formularios Basados en Plantillas
- Formularios Reactivos
- Validación de Formularios
- Formularios Dinámicos
Módulo 7: Cliente HTTP y Observables
- Introducción al Cliente HTTP
- Realizando Solicitudes HTTP
- Manejo de Respuestas HTTP
- Uso de Observables
- Manejo de Errores
Módulo 8: Gestión de Estado
- Introducción a la Gestión de Estado
- Uso de Servicios para la Gestión de Estado
- NgRx Store
- NgRx Effects
- NgRx Entity
Módulo 9: Pruebas en Angular
- Pruebas Unitarias
- Pruebas de Componentes
- Pruebas de Servicios
- Pruebas de Extremo a Extremo
- Simulación de Dependencias
Módulo 10: Conceptos Avanzados de Angular
- Angular Universal
- Optimización del Rendimiento
- Internacionalización (i18n)
- Pipes Personalizados
- Animaciones en Angular