Mocking servicios con jasmine.createSpy
Los mocks y espías son fundamentales para aislar el componente bajo prueba y verificar interacciones con sus dependencias. En el contexto del testing, un mock es un objeto falso que simula el comportamiento de una dependencia real, mientras que un espía (spy) observa y registra las llamadas realizadas a métodos específicos.
Fundamentos teóricos del mocking
Cuando testeas un componente que depende de servicios, necesitas aislar la lógica del componente de la lógica del servicio. Si permites que el test ejecute el servicio real, estarías haciendo un test de integración en lugar de un test unitario. Los mocks nos permiten:
- Controlar el comportamiento de las dependencias
- Verificar las interacciones entre el componente y sus servicios
- Simular diferentes escenarios (éxito, error, datos específicos)
- Acelerar la ejecución de los tests al evitar llamadas reales
jasmine.createSpy() para métodos individuales
La función jasmine.createSpy() crea un espía independiente que puede simular cualquier función. Es útil cuando necesitas mockear un método específico sin crear un objeto completo:
describe('UserComponent', () => {
let component: UserComponent;
let fixture: ComponentFixture<UserComponent>;
let mockGetUser: jasmine.Spy;
beforeEach(() => {
// Crear un espía para un método específico
mockGetUser = jasmine.createSpy('getUser');
TestBed.configureTestingModule({
imports: [UserComponent],
providers: [
{ provide: UserService, useValue: { getUser: mockGetUser } }
]
});
fixture = TestBed.createComponent(UserComponent);
component = fixture.componentInstance;
});
});
Configurando comportamientos del espía
Los espías creados con createSpy() pueden configurarse para retornar valores específicos o simular diferentes comportamientos:
it('should display user name when service returns data', () => {
// Configurar el espía para retornar un valor
mockGetUser.and.returnValue('Juan Pérez');
component.loadUser(1);
fixture.detectChanges();
expect(mockGetUser).toHaveBeenCalledWith(1);
expect(component.userName).toBe('Juan Pérez');
});
it('should handle service error', () => {
// Configurar el espía para lanzar un error
mockGetUser.and.throwError('User not found');
component.loadUser(999);
expect(mockGetUser).toHaveBeenCalledWith(999);
expect(component.errorMessage).toBe('Error loading user');
});
Espías para métodos asíncronos
Cuando el servicio retorna Observables o Promises, puedes configurar el espía para simular comportamiento asíncrono:
it('should handle async service call', () => {
const mockUserData = { id: 1, name: 'Ana García' };
// Simular Observable que emite un valor
mockGetUser.and.returnValue(of(mockUserData));
component.loadUserAsync(1);
fixture.detectChanges();
expect(mockGetUser).toHaveBeenCalledWith(1);
expect(component.user).toEqual(mockUserData);
});
Verificando interacciones con espías
Los espías registran todas las llamadas realizadas, permitiendo verificar cómo interactúa el componente con sus dependencias:
it('should call service with correct parameters', () => {
mockGetUser.and.returnValue('Test User');
component.searchUser('test@example.com');
// Verificar que se llamó al método
expect(mockGetUser).toHaveBeenCalled();
// Verificar parámetros específicos
expect(mockGetUser).toHaveBeenCalledWith('test@example.com');
// Verificar número de llamadas
expect(mockGetUser).toHaveBeenCalledTimes(1);
});
Reseteando espías entre tests
Es importante limpiar el estado de los espías entre diferentes tests para evitar interferencias:
beforeEach(() => {
// Limpiar historial de llamadas del espía
mockGetUser.calls.reset();
// O crear un nuevo espía limpio
mockGetUser = jasmine.createSpy('getUser');
});
Ejemplo práctico completo
Consideremos un componente simple que muestra información del usuario:
// user-display.component.ts
@Component({
selector: 'app-user-display',
standalone: true,
template: `
<div>
@if (loading) {
<p>Cargando...</p>
} @else if (user) {
<h2>{{ user.name }}</h2>
<p>{{ user.email }}</p>
} @else {
<p>Usuario no encontrado</p>
}
</div>
`
})
export class UserDisplayComponent {
user: User | null = null;
loading = false;
constructor(private userService: UserService) {}
loadUser(id: number): void {
this.loading = true;
const userData = this.userService.getUserById(id);
this.user = userData;
this.loading = false;
}
}
El test correspondiente utilizando jasmine.createSpy():
describe('UserDisplayComponent', () => {
let component: UserDisplayComponent;
let fixture: ComponentFixture<UserDisplayComponent>;
let mockGetUserById: jasmine.Spy;
beforeEach(() => {
mockGetUserById = jasmine.createSpy('getUserById');
TestBed.configureTestingModule({
imports: [UserDisplayComponent],
providers: [
{
provide: UserService,
useValue: { getUserById: mockGetUserById }
}
]
});
fixture = TestBed.createComponent(UserDisplayComponent);
component = fixture.componentInstance;
});
it('should display user information when service returns data', () => {
const mockUser = { id: 1, name: 'Carlos López', email: 'carlos@test.com' };
mockGetUserById.and.returnValue(mockUser);
component.loadUser(1);
fixture.detectChanges();
expect(mockGetUserById).toHaveBeenCalledWith(1);
expect(component.user).toEqual(mockUser);
expect(fixture.nativeElement.textContent).toContain('Carlos López');
});
it('should handle when user is not found', () => {
mockGetUserById.and.returnValue(null);
component.loadUser(999);
fixture.detectChanges();
expect(mockGetUserById).toHaveBeenCalledWith(999);
expect(component.user).toBeNull();
expect(fixture.nativeElement.textContent).toContain('Usuario no encontrado');
});
});
Este enfoque permite testear el comportamiento del componente sin depender de la implementación real del servicio, garantizando tests rápidos, predecibles y enfocados en la lógica específica del componente.
Testing básico de componentes con services
Una vez que dominas la creación de espías, el siguiente paso es integrar estos mocks en el testing de componentes reales que dependen de servicios. El TestBed de Angular proporciona las herramientas necesarias para reemplazar dependencias y crear un entorno controlado de testing.
Provider overrides en TestBed
El TestBed.configureTestingModule() permite sobrescribir providers para reemplazar servicios reales con mocks. Esto se hace mediante la propiedad providers que acepta diferentes estrategias de reemplazo:
describe('ProductListComponent', () => {
let component: ProductListComponent;
let fixture: ComponentFixture<ProductListComponent>;
let mockProductService: jasmine.SpyObj<ProductService>;
beforeEach(() => {
// Crear un objeto mock completo del servicio
mockProductService = jasmine.createSpyObj('ProductService', [
'getProducts',
'deleteProduct'
]);
TestBed.configureTestingModule({
imports: [ProductListComponent],
providers: [
// Reemplazar ProductService con el mock
{ provide: ProductService, useValue: mockProductService }
]
});
fixture = TestBed.createComponent(ProductListComponent);
component = fixture.componentInstance;
});
});
Testing de inicialización con servicios
Muchos componentes cargan datos en ngOnInit() o en el constructor. Es crucial testear este comportamiento de inicialización:
it('should load products on initialization', () => {
const mockProducts = [
{ id: 1, name: 'Laptop', price: 999 },
{ id: 2, name: 'Mouse', price: 25 }
];
// Configurar el comportamiento del mock antes de la inicialización
mockProductService.getProducts.and.returnValue(mockProducts);
// Disparar la inicialización del componente
component.ngOnInit();
fixture.detectChanges();
expect(mockProductService.getProducts).toHaveBeenCalled();
expect(component.products).toEqual(mockProducts);
expect(component.products.length).toBe(2);
});
Testing de interacciones del usuario con servicios
Los componentes frecuentemente invocan servicios en respuesta a acciones del usuario. Debes testear tanto la interacción como la respuesta del componente:
it('should delete product when user clicks delete button', () => {
const productToDelete = { id: 1, name: 'Laptop', price: 999 };
component.products = [productToDelete];
// Configurar el mock para simular eliminación exitosa
mockProductService.deleteProduct.and.returnValue(true);
// Simular la acción del usuario
component.deleteProduct(productToDelete.id);
fixture.detectChanges();
expect(mockProductService.deleteProduct).toHaveBeenCalledWith(1);
expect(component.products.length).toBe(0);
});
Manejo de estados de carga y error
Los componentes reales manejan estados asíncronos como loading y error. Testa estos estados para asegurar una buena experiencia de usuario:
it('should show loading state while fetching products', () => {
component.loading = true;
fixture.detectChanges();
const loadingElement = fixture.nativeElement.querySelector('.loading');
expect(loadingElement).toBeTruthy();
expect(loadingElement.textContent).toContain('Cargando productos');
});
it('should handle service errors gracefully', () => {
mockProductService.getProducts.and.throwError('Server error');
component.loadProducts();
fixture.detectChanges();
expect(component.error).toBeTruthy();
expect(component.products).toEqual([]);
const errorElement = fixture.nativeElement.querySelector('.error-message');
expect(errorElement.textContent).toContain('Error al cargar productos');
});
Testing con fakeAsync y tick
Para servicios que retornan Observables o Promises, utiliza fakeAsync y tick() para controlar el tiempo de ejecución:
import { fakeAsync, tick } from '@angular/core/testing';
import { of, delay } from 'rxjs';
it('should handle delayed service response', fakeAsync(() => {
const mockProducts = [{ id: 1, name: 'Product 1' }];
// Simular Observable con delay
mockProductService.getProducts.and.returnValue(
of(mockProducts).pipe(delay(1000))
);
component.loadProducts();
expect(component.loading).toBe(true);
// Avanzar el tiempo virtual
tick(1000);
fixture.detectChanges();
expect(component.loading).toBe(false);
expect(component.products).toEqual(mockProducts);
}));
Testing de múltiples dependencias
Los componentes complejos pueden depender de varios servicios. Organiza los mocks de manera clara y testea las interacciones entre ellos:
describe('OrderComponent', () => {
let component: OrderComponent;
let fixture: ComponentFixture<OrderComponent>;
let mockOrderService: jasmine.SpyObj<OrderService>;
let mockNotificationService: jasmine.SpyObj<NotificationService>;
beforeEach(() => {
mockOrderService = jasmine.createSpyObj('OrderService', ['createOrder']);
mockNotificationService = jasmine.createSpyObj('NotificationService', ['showSuccess']);
TestBed.configureTestingModule({
imports: [OrderComponent],
providers: [
{ provide: OrderService, useValue: mockOrderService },
{ provide: NotificationService, useValue: mockNotificationService }
]
});
fixture = TestBed.createComponent(OrderComponent);
component = fixture.componentInstance;
});
it('should create order and show notification', () => {
const orderData = { productId: 1, quantity: 2 };
mockOrderService.createOrder.and.returnValue({ id: 123, ...orderData });
component.submitOrder(orderData);
expect(mockOrderService.createOrder).toHaveBeenCalledWith(orderData);
expect(mockNotificationService.showSuccess).toHaveBeenCalledWith('Pedido creado exitosamente');
});
});
Ejemplo práctico: Componente con servicio HTTP
Consideremos un componente que gestiona una lista de tareas usando un servicio HTTP:
// task-manager.component.ts
@Component({
selector: 'app-task-manager',
standalone: true,
template: `
<div>
@if (loading) {
<div class="loading">Cargando tareas...</div>
}
@for (task of tasks; track task.id) {
<div class="task">
<span>{{ task.title }}</span>
<button (click)="completeTask(task.id)">Completar</button>
</div>
}
@if (error) {
<div class="error">{{ error }}</div>
}
</div>
`
})
export class TaskManagerComponent implements OnInit {
tasks: Task[] = [];
loading = false;
error: string | null = null;
constructor(private taskService: TaskService) {}
ngOnInit(): void {
this.loadTasks();
}
loadTasks(): void {
this.loading = true;
this.error = null;
try {
this.tasks = this.taskService.getAllTasks();
this.loading = false;
} catch (error) {
this.error = 'Error al cargar las tareas';
this.loading = false;
}
}
completeTask(taskId: number): void {
this.taskService.markAsCompleted(taskId);
this.loadTasks(); // Recargar la lista
}
}
El test completo que verifica la integración con el servicio:
describe('TaskManagerComponent', () => {
let component: TaskManagerComponent;
let fixture: ComponentFixture<TaskManagerComponent>;
let mockTaskService: jasmine.SpyObj<TaskService>;
beforeEach(() => {
mockTaskService = jasmine.createSpyObj('TaskService', [
'getAllTasks',
'markAsCompleted'
]);
TestBed.configureTestingModule({
imports: [TaskManagerComponent],
providers: [
{ provide: TaskService, useValue: mockTaskService }
]
});
fixture = TestBed.createComponent(TaskManagerComponent);
component = fixture.componentInstance;
});
it('should load tasks on component initialization', () => {
const mockTasks = [
{ id: 1, title: 'Tarea 1', completed: false },
{ id: 2, title: 'Tarea 2', completed: true }
];
mockTaskService.getAllTasks.and.returnValue(mockTasks);
component.ngOnInit();
fixture.detectChanges();
expect(mockTaskService.getAllTasks).toHaveBeenCalled();
expect(component.tasks).toEqual(mockTasks);
expect(component.loading).toBe(false);
});
it('should complete task when button is clicked', () => {
const initialTasks = [{ id: 1, title: 'Tarea pendiente', completed: false }];
const updatedTasks = [{ id: 1, title: 'Tarea pendiente', completed: true }];
// Configurar comportamiento secuencial del mock
mockTaskService.getAllTasks.and.returnValues(initialTasks, updatedTasks);
component.tasks = initialTasks;
fixture.detectChanges();
// Simular click en el botón
component.completeTask(1);
expect(mockTaskService.markAsCompleted).toHaveBeenCalledWith(1);
expect(mockTaskService.getAllTasks).toHaveBeenCalledTimes(1);
});
it('should display error message when service fails', () => {
mockTaskService.getAllTasks.and.throwError('Network error');
component.loadTasks();
fixture.detectChanges();
expect(component.error).toBe('Error al cargar las tareas');
expect(component.loading).toBe(false);
const errorElement = fixture.nativeElement.querySelector('.error');
expect(errorElement.textContent).toContain('Error al cargar las tareas');
});
});
Este enfoque permite testear componentes reales con sus dependencias de servicios de manera controlada y predecible, asegurando que el componente maneje correctamente tanto los casos exitosos como los escenarios de error.

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 el concepto y la importancia del mocking y los espías en tests unitarios.
- Aprender a crear y configurar espías con jasmine.createSpy para simular métodos de servicios.
- Integrar mocks en TestBed para reemplazar servicios reales en tests de componentes.
- Testear comportamientos síncronos y asíncronos, incluyendo manejo de errores y estados de carga.
- Gestionar múltiples dependencias y verificar interacciones entre componentes y servicios en tests.