Testing de servicios

Intermedio
Angular
Angular
Actualizado: 24/09/2025

Configuración de tests con TestBed

El testing en Angular es fundamental para garantizar que nuestras aplicaciones funcionen correctamente y mantengan su calidad a lo largo del tiempo. Angular proporciona un ecosistema robusto de testing que nos permite verificar tanto la lógica de negocio como la integración entre componentes y servicios.

Introducción al testing en Angular

Angular viene configurado por defecto con herramientas de testing modernas. En Angular 20+, el framework ha evolucionado hacia el uso de Vitest como runner principal, reemplazando gradualmente a Karma, aunque los conceptos fundamentales de testing permanecen universales y aplicables a cualquier herramienta.

Los tests en Angular se organizan en archivos con extensión .spec.ts y siguen el patrón AAA (Arrange, Act, Assert):

  • Arrange: Configuramos el entorno de testing
  • Act: Ejecutamos la funcionalidad a testear
  • Assert: Verificamos que el resultado sea el esperado

¿Qué es TestBed?

TestBed es la utilidad principal de Angular para configurar y crear un módulo de testing dinámico. Actúa como un mini-framework que simula el entorno de ejecución de Angular, permitiéndonos instanciar servicios, componentes y otros elementos de forma aislada para testing.

TestBed nos proporciona:

  • Configuración declarativa del entorno de testing
  • Inyección de dependencias controlada
  • Aislamiento de cada test
  • Simulación del contexto de ejecución de Angular

Estructura básica de un test de servicio

Veamos la estructura fundamental de un test de servicio en Angular:

import { TestBed } from '@angular/core/testing';
import { MiServicio } from './mi-servicio.service';

describe('MiServicio', () => {
  let service: MiServicio;

  beforeEach(() => {
    TestBed.configureTestingModule({});
    service = TestBed.inject(MiServicio);
  });

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

Configuración con TestBed.configureTestingModule()

El método configureTestingModule() es donde definimos la configuración específica para nuestros tests. Acepta un objeto de configuración similar al que usaríamos en un módulo de Angular tradicional:

beforeEach(() => {
  TestBed.configureTestingModule({
    imports: [HttpClientTestingModule, RouterTestingModule],
    providers: [
      MiServicio,
      { provide: ConfigService, useValue: mockConfig }
    ]
  });
  
  service = TestBed.inject(MiServicio);
});

Los parámetros más comunes de configuración incluyen:

  • imports: Módulos necesarios para el test
  • providers: Servicios y sus configuraciones
  • declarations: Componentes, directivas y pipes (cuando sea necesario)

Inyección de servicios con TestBed.inject()

Una vez configurado el TestBed, utilizamos TestBed.inject() para obtener instancias de los servicios que queremos testear. Este método reemplaza al antiguo TestBed.get() y proporciona mejor tipado:

// Inyección básica
const miServicio = TestBed.inject(MiServicio);

// Inyección con token
const httpClient = TestBed.inject(HttpClient);

// Inyección de servicios con interfaces
const logger = TestBed.inject(LoggerService);

Configuración para servicios con dependencias

Cuando nuestros servicios tienen dependencias, necesitamos configurarlas en el TestBed. Esto es especialmente importante para servicios que dependen de HttpClient u otros servicios externos:

import { HttpClientTestingModule } from '@angular/common/http/testing';

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

Configuración para servicios standalone

Con la arquitectura moderna de Angular 20+, muchos servicios se definen con providedIn: 'root'. Para estos servicios, la configuración es más simple:

// Servicio con providedIn: 'root'
@Injectable({ providedIn: 'root' })
export class UserService {
  constructor(private http: HttpClient) {}
}

// Test configuration
beforeEach(() => {
  TestBed.configureTestingModule({
    imports: [HttpClientTestingModule]
  });
  
  service = TestBed.inject(UserService);
});

Limpieza entre tests

Angular automáticamente limpia el TestBed entre cada test, pero es una buena práctica ser explícito sobre la configuración. Cada beforeEach() recrea el entorno de testing de forma aislada:

describe('UserService', () => {
  let service: UserService;

  beforeEach(() => {
    // Configuración limpia para cada test
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule]
    });
    
    service = TestBed.inject(UserService);
  });

  // Cada test comienza con un entorno limpio
  it('should initialize with default values', () => {
    expect(service).toBeTruthy();
  });
});

Configuración para testing de signals

Dado que trabajamos con Angular 20+ y signals, nuestros servicios pueden usar signals para gestionar estado. La configuración del TestBed permanece igual, ya que los signals funcionan nativamente:

@Injectable({ providedIn: 'root' })
export class StateService {
  private _count = signal(0);
  count = this._count.asReadonly();
  
  increment() {
    this._count.update(current => current + 1);
  }
}

// La configuración es idéntica
beforeEach(() => {
  TestBed.configureTestingModule({});
  service = TestBed.inject(StateService);
});

Esta configuración básica con TestBed nos proporciona la base sólida necesaria para escribir tests efectivos de servicios en Angular, estableciendo el entorno aislado y controlado que necesitamos para verificar el comportamiento de nuestro código.

Testing básico de métodos y HTTP calls

Una vez que tenemos configurado nuestro entorno de testing con TestBed, podemos proceder a testear la funcionalidad real de nuestros servicios. Esto incluye verificar que los métodos funcionen correctamente y que las llamadas HTTP se comporten como esperamos.

Testing de métodos básicos

Los métodos básicos de un servicio son aquellos que no requieren dependencias externas y realizan operaciones síncronas. Estos son ideales para comenzar con el testing:

@Injectable({ providedIn: 'root' })
export class CalculatorService {
  add(a: number, b: number): number {
    return a + b;
  }
  
  multiply(a: number, b: number): number {
    return a * b;
  }
  
  isEven(num: number): boolean {
    return num % 2 === 0;
  }
}

Los tests para estos métodos son directos y utilizan assertions básicas:

describe('CalculatorService', () => {
  let service: CalculatorService;

  beforeEach(() => {
    TestBed.configureTestingModule({});
    service = TestBed.inject(CalculatorService);
  });

  it('should add two numbers correctly', () => {
    const result = service.add(3, 5);
    expect(result).toBe(8);
  });

  it('should multiply two numbers correctly', () => {
    const result = service.multiply(4, 6);
    expect(result).toBe(24);
  });

  it('should identify even numbers', () => {
    expect(service.isEven(4)).toBe(true);
    expect(service.isEven(7)).toBe(false);
  });
});

Testing de servicios con estado usando signals

Para servicios que manejan estado con signals, verificamos tanto la lectura como la modificación del estado:

@Injectable({ providedIn: 'root' })
export class CounterService {
  private _count = signal(0);
  count = this._count.asReadonly();
  
  increment(): void {
    this._count.update(current => current + 1);
  }
  
  reset(): void {
    this._count.set(0);
  }
  
  setCount(value: number): void {
    this._count.set(value);
  }
}

Los tests verifican el comportamiento reactivo de los signals:

it('should start with count of 0', () => {
  expect(service.count()).toBe(0);
});

it('should increment count by 1', () => {
  service.increment();
  expect(service.count()).toBe(1);
  
  service.increment();
  expect(service.count()).toBe(2);
});

it('should reset count to 0', () => {
  service.setCount(5);
  service.reset();
  expect(service.count()).toBe(0);
});

it('should set count to specific value', () => {
  service.setCount(10);
  expect(service.count()).toBe(10);
});

Introducción al testing de HTTP calls

Cuando nuestros servicios realizan llamadas HTTP, necesitamos una forma de interceptar y simular estas llamadas durante los tests. Angular proporciona HttpTestingController para este propósito, que nos permite controlar las peticiones HTTP sin realizar llamadas reales a un servidor.

Primero, veamos un servicio básico que hace llamadas HTTP:

@Injectable({ providedIn: 'root' })
export class UserService {
  private apiUrl = 'https://api.example.com/users';
  
  constructor(private http: HttpClient) {}
  
  getUsers(): Observable<User[]> {
    return this.http.get<User[]>(this.apiUrl);
  }
  
  getUserById(id: number): Observable<User> {
    return this.http.get<User>(`${this.apiUrl}/${id}`);
  }
  
  createUser(user: Partial<User>): Observable<User> {
    return this.http.post<User>(this.apiUrl, user);
  }
}

Configuración para testing HTTP

Para testear servicios con HTTP, necesitamos importar HttpClientTestingModule y usar HttpTestingController:

import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';

describe('UserService', () => {
  let service: UserService;
  let httpTestingController: HttpTestingController;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule]
    });
    
    service = TestBed.inject(UserService);
    httpTestingController = TestBed.inject(HttpTestingController);
  });

  afterEach(() => {
    // Verificar que no hay requests pendientes
    httpTestingController.verify();
  });
});

Testing básico de GET requests

Para testear una petición GET, utilizamos el patrón de suscribirnos al Observable y verificar tanto la petición como la respuesta:

it('should get users list', () => {
  const mockUsers: User[] = [
    { id: 1, name: 'Juan', email: 'juan@example.com' },
    { id: 2, name: 'Ana', email: 'ana@example.com' }
  ];

  service.getUsers().subscribe(users => {
    expect(users).toEqual(mockUsers);
    expect(users.length).toBe(2);
  });

  // Interceptar la petición HTTP
  const req = httpTestingController.expectOne('https://api.example.com/users');
  
  // Verificar que es una petición GET
  expect(req.request.method).toBe('GET');
  
  // Simular la respuesta del servidor
  req.flush(mockUsers);
});

Testing de GET con parámetros

Para peticiones que incluyen parámetros en la URL:

it('should get user by id', () => {
  const mockUser: User = { id: 1, name: 'Juan', email: 'juan@example.com' };

  service.getUserById(1).subscribe(user => {
    expect(user).toEqual(mockUser);
    expect(user.id).toBe(1);
  });

  const req = httpTestingController.expectOne('https://api.example.com/users/1');
  expect(req.request.method).toBe('GET');
  
  req.flush(mockUser);
});

Testing básico de POST requests

Para testear peticiones POST, verificamos tanto el cuerpo de la petición como la respuesta:

it('should create new user', () => {
  const newUser: Partial<User> = { name: 'Carlos', email: 'carlos@example.com' };
  const createdUser: User = { id: 3, name: 'Carlos', email: 'carlos@example.com' };

  service.createUser(newUser).subscribe(user => {
    expect(user).toEqual(createdUser);
    expect(user.id).toBeDefined();
  });

  const req = httpTestingController.expectOne('https://api.example.com/users');
  expect(req.request.method).toBe('POST');
  expect(req.request.body).toEqual(newUser);
  
  req.flush(createdUser);
});

Assertions básicas con expect()

Las assertions son la parte fundamental de nuestros tests. Utilizamos diferentes matchers de Jasmine según lo que queremos verificar:

  • toBe(): Para comparación exacta (===)
  • toEqual(): Para comparación profunda de objetos
  • toBeTruthy() / toBeFalsy(): Para verificar valores verdaderos o falsos
  • toContain(): Para verificar que un array contiene un elemento
  • toBeDefined() / toBeUndefined(): Para verificar si algo está definido
it('should demonstrate basic assertions', () => {
  const user = { id: 1, name: 'Test User' };
  const numbers = [1, 2, 3, 4, 5];
  
  // Comparaciones exactas
  expect(user.id).toBe(1);
  expect(user.name).toBe('Test User');
  
  // Comparaciones de objetos
  expect(user).toEqual({ id: 1, name: 'Test User' });
  
  // Arrays
  expect(numbers).toContain(3);
  expect(numbers.length).toBe(5);
  
  // Valores definidos
  expect(user.id).toBeDefined();
  expect(user.age).toBeUndefined();
});

Manejo básico de errores HTTP

También podemos testear cómo nuestros servicios manejan errores HTTP:

it('should handle HTTP error', () => {
  service.getUsers().subscribe({
    next: () => fail('Expected error'),
    error: (error) => {
      expect(error.status).toBe(500);
    }
  });

  const req = httpTestingController.expectOne('https://api.example.com/users');
  
  // Simular un error del servidor
  req.flush('Server Error', { status: 500, statusText: 'Internal Server Error' });
});

Este enfoque básico de testing nos proporciona las herramientas fundamentales para verificar que nuestros servicios funcionen correctamente, tanto en su lógica interna como en su comunicación con APIs externas, estableciendo una base sólida para escribir tests confiables y mantenibles.

Alan Sastre - Autor del tutorial

Alan Sastre

Ingeniero de Software y formador, CEO en CertiDevs

Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Angular es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.

Más tutoriales de Angular

Explora más contenido relacionado con Angular y continúa aprendiendo con nuestros tutoriales gratuitos.

Aprendizajes de esta lección

  • Comprender la configuración básica de tests en Angular con TestBed.
  • Aprender a inyectar servicios y configurar dependencias para testing.
  • Realizar tests de métodos síncronos y servicios con estado usando signals.
  • Testear llamadas HTTP simulando peticiones con HttpTestingController.
  • Manejar assertions básicas y pruebas de errores en servicios Angular.