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.

  1. 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"
    }
    
  2. 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.

  1. Generar el Servicio:

    ng generate service data
    
  2. 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.

  1. 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. Importamos HttpClientTestingModule 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.

  1. 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;
      }
    }
    
  2. 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');
        }
      }
    }
    
  3. 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: Utilizamos jasmine.createSpyObj para crear un mock del AuthService y simular su método isAuthenticated.
  • 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

  1. 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.
  2. 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

  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);
      }
    }
    
  2. 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

  1. 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');
        }
      }
    }
    
  2. 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

Módulo 2: Componentes de Angular

Módulo 3: Enlace de Datos y Directivas

Módulo 4: Servicios e Inyección de Dependencias

Módulo 5: Enrutamiento y Navegación

Módulo 6: Formularios en Angular

Módulo 7: Cliente HTTP y Observables

Módulo 8: Gestión de Estado

Módulo 9: Pruebas en Angular

Módulo 10: Conceptos Avanzados de Angular

Módulo 11: Despliegue y Mejores Prácticas

© Copyright 2024. Todos los derechos reservados