Testing de componentes con dependencias

Intermedio
Angular
Angular
Actualizado: 24/09/2025

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