ANG_FRONT

Práctica 13. Pruebas unitarias en Angular: Asegurando la calidad del código

Objetivo de la práctica

Duración aproximada

Instrucciones

  1. Métodos públicos.

export class Calculadora {
  sumar(a: number, b: number): number {
    return a + b;
  }
}

import { Calculadora } from './calculadora';

describe('Calculadora', () => {
  let calculadora: Calculadora;

  beforeEach(() => {
    calculadora = new Calculadora();
  });

  it('debería sumar dos números correctamente', () => {
    const resultado = calculadora.sumar(2, 3);
    expect(resultado).toBe(5);
  });
});

  1. Métodos privados.

export class Calculadora {
  public sumar(a: number, b: number): number {
    if (!this.validarNumeros(a, b)) {
      throw new Error('Números no válidos');
    }
    return a + b;
  }

  private validarNumeros(a: number, b: number): boolean {
    return typeof a === 'number' && typeof b === 'number';
  }
}

describe('Calculadora', () => {
  let calculadora: Calculadora;

  beforeEach(() => {
    calculadora = new Calculadora();
  });

  it('debería lanzar un error si los números no son válidos', () => {
    expect(() => calculadora.sumar('a', 3)).toThrowError('Números no válidos');
  });
});

  1. Métodos sin valor de retorno.

export class Contador {
  private cuenta: number = 0;

  public incrementar(): void {
    this.cuenta++;
  }

  public obtenerCuenta(): number {
    return this.cuenta;
  }
}

describe('Contador', () => {
  let contador: Contador;

  beforeEach(() => {
    contador = new Contador();
  });

  it('debería incrementar la cuenta', () => {
    contador.incrementar();
    expect(contador.obtenerCuenta()).toBe(1);
  });
});
  1. Buenas prácticas para probar métodos.

Test de suscripciones (subscribe-observable) en Angular

  1. ¿Qué son los Observables?

import { Observable } from 'rxjs';

const miObservable = new Observable(observer => {
  observer.next('Primer valor');
  observer.next('Segundo valor');
  observer.complete();
});
  1. La Función subscribe.

miObservable.subscribe(
  valor => console.log(valor), // Manejar valores emitidos
  error => console.error(error), // Manejar errores
  () => console.log('Completo') // Manejar finalización
);
  1. Test de suscripciones en componentes.

import { Component, OnInit } from '@angular/core';
import { MiServicio } from './mi-servicio.service';

@Component({
  selector: 'app-mi-componente',
  template: `<div></div>`
})
export class MiComponente implements OnInit {
  dato: string;

  constructor(private miServicio: MiServicio) {}

  ngOnInit() {
    this.miServicio.obtenerDatos().subscribe(valor => {
      this.dato = valor;
    });
  }
}
  1. Prueba del componente con suscripción.

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MiComponente } from './mi-componente.component';
import { MiServicio } from './mi-servicio.service';
import { of } from 'rxjs';

describe('MiComponente', () => {
  let component: MiComponente;
  let fixture: ComponentFixture<MiComponente>;
  let miServicio: jasmine.SpyObj<MiServicio>;

  beforeEach(async () => {
    const servicioSpy = jasmine.createSpyObj('MiServicio', ['obtenerDatos']);
    
    await TestBed.configureTestingModule({
      declarations: [MiComponente],
      providers: [{ provide: MiServicio, useValue: servicioSpy }]
    }).compileComponents();

    miServicio = TestBed.inject(MiServicio) as jasmine.SpyObj<MiServicio>;
  });

  beforeEach(() => {
    fixture = TestBed.createComponent(MiComponente);
    component = fixture.componentInstance;
  });

  it('debería asignar el dato al recibir un valor del servicio', () => {
    miServicio.obtenerDatos.and.returnValue(of('Valor de prueba'));
    
    component.ngOnInit(); // Llamar a ngOnInit para iniciar la suscripción
    fixture.detectChanges(); // Detectar cambios

    expect(component.dato).toBe('Valor de prueba');
  });
});

  1. Buenas prácticas para probar suscripciones.

Test de Pipes en Angular

  1. ¿Qué son los Pipes?

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'capitalizar'
})
export class CapitalizarPipe implements PipeTransform {
  transform(value: string): string {
    return value ? value.charAt(0).toUpperCase() + value.slice(1) : '';
  }
}

En este ejemplo, el Pipe CapitalizarPipe transforma la primera letra de un texto a mayúscula.

  1. Pruebas de Pipes.

import { CapitalizarPipe } from './capitalizar.pipe';

describe('CapitalizarPipe', () => {
  let pipe: CapitalizarPipe;

  beforeEach(() => {
    pipe = new CapitalizarPipe();
  });

  it('debería capitalizar la primera letra de una cadena', () => {
    expect(pipe.transform('hola')).toBe('Hola');
  });

  it('debería retornar una cadena vacía si el valor es vacío', () => {
    expect(pipe.transform('')).toBe('');
  });

  it('debería manejar valores nulos', () => {
    expect(pipe.transform(null)).toBe('');
  });

  it('debería capitalizar solo la primera letra, dejando el resto igual', () => {
    expect(pipe.transform('hOLA')).toBe('HOLA');
  });
});
  1. Buenas prácticas al probar Pipes.

Verificar diferentes casos de entrada: Asegurarse de probar una variedad de entradas, incluyendo cadenas vacías, nulas y valores especiales.

Mantener las pruebas simples: Dado que los Pipes son funciones puras, las pruebas deben ser simples y centrarse en la entrada y salida del método transform.

Utilizar descripciones claras: Usar descripciones claras en las pruebas para que sea evidente lo que se está validando en cada caso.

  1. Ejecución de pruebas.

ng test

Test de servicios (con Peticiones a API) en Angular

  1. ¿Qué es un servicio en Angular?

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

@Injectable({
  providedIn: 'root',
})
export class ApiService {
  private apiUrl = 'https://api.example.com/datos';

  constructor(private http: HttpClient) {}

  obtenerDatos(): Observable<any> {
    return this.http.get(this.apiUrl);
  }
}
  1. Pruebas de servicios con peticiones a API.

import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { ApiService } from './api.service';

describe('ApiService', () => {
  let service: ApiService;
  let httpMock: HttpTestingController;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
      providers: [ApiService],
    });

    service = TestBed.inject(ApiService);
    httpMock = TestBed.inject(HttpTestingController);
  });

  afterEach(() => {
    httpMock.verify(); // Verifica que no haya solicitudes HTTP pendientes
  });
});
  1. Ejemplo de pruebas

it('debería retornar datos al realizar una petición exitosa', () => {
  const mockDatos = [{ id: 1, nombre: 'Dato 1' }, { id: 2, nombre: 'Dato 2' }];

  service.obtenerDatos().subscribe(datos => {
    expect(datos).toEqual(mockDatos);
  });

  const req = httpMock.expectOne('https://api.example.com/datos');
  expect(req.request.method).toBe('GET'); // Verificar el método de la solicitud
  req.flush(mockDatos); // Simular la respuesta de la API
});

it('debería manejar errores al realizar la petición', () => {
  const errorMsg = 'Error de red';

  service.obtenerDatos().subscribe(
    () => fail('Se esperaba un error, no datos'),
    error => {
      expect(error.status).toBe(500);
      expect(error.error).toContain(errorMsg);
    }
  );

  const req = httpMock.expectOne('https://api.example.com/datos');
  req.flush(errorMsg, { status: 500, statusText: 'Error de red' }); // Simular un error
});
  1. Buenas prácticas al probar servicios.

Utilizar HttpClientTestingModule: Esto permite realizar pruebas sin necesidad de realizar llamadas reales a la API.

Verificar que no queden solicitudes pendientes: Siempre verifica que no haya solicitudes HTTP no respondidas utilizando httpMock.verify().

Probar casos positivos y negativos: Asegurarse de probar tanto las respuestas exitosas como los escenarios de error para tener una cobertura completa.

Utilizar mocks y spies: Para servicios que dependen de otros servicios, utiliza mocks y spies para aislar las pruebas y verificar el comportamiento de los servicios sin depender de su implementación.

Mock de servicios en Angular

  1. ¿Qué es un Servicio en Angular?

Un servicio en Angular es una clase que contiene lógica de negocio y funcionalidades que pueden ser compartidas entre diferentes componentes de la aplicación. Los servicios son una parte esencial de la arquitectura de Angular, ya que permiten organizar y encapsular el código de manera eficiente.


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

@Injectable({
  providedIn: 'root',
})
export class ApiService {
  constructor(private http: HttpClient) {}

  obtenerDatos(): Observable<any> {
    return this.http.get('https://api.example.com/datos');
  }
}
  1. ¿Por qué usar Mocks?
  1. Cómo crear un mock de servicio.

-Existen diferentes maneras de crear un mock de servicio en Angular. Una de las más comunes es usar clases simuladas que implementen la misma interfaz que el servicio real.

-Ejemplo de un mock de servicio


class MockApiService {
  obtenerDatos() {
    return of([{ id: 1, nombre: 'Dato 1' }, { id: 2, nombre: 'Dato 2' }]);
  }
}
  1. Usar el mock en pruebas.

import { TestBed } from '@angular/core/testing';
import { MiComponente } from './mi-componente.component';
import { ApiService } from './api.service';
import { MockApiService } from './mock-api.service'; // Importar el mock

describe('MiComponente', () => {
  let component: MiComponente;

  beforeEach(() => {
    TestBed.configureTestingModule({
      declarations: [MiComponente],
      providers: [{ provide: ApiService, useClass: MockApiService }] // Usar el mock
    });

    const fixture = TestBed.createComponent(MiComponente);
    component = fixture.componentInstance;
  });

  it('debería obtener datos al inicializar', () => {
    component.ngOnInit();
    expect(component.datos.length).toBe(2);
    expect(component.datos[0].nombre).toBe('Dato 1');
  });
});
  1. Buenas prácticas al usar mocks.

Aislar pruebas: Siempre que sea posible, utilizar mocks para aislar las pruebas de los componentes o servicios de su lógica subyacente.

Simular respuestas realistas: Asegurarse de que el comportamiento del mock sea lo más similar posible al servicio real para que las pruebas sean significativas.

Probar diferentes escenarios: Crear diferentes mocks o configura el mismo mock para simular distintos escenarios, como respuestas exitosas o errores.

Limitar dependencias: Reducir la complejidad de las pruebas limitando las dependencias a lo estrictamente necesario.