🇺🇸 EN
Angular
·9 min read·↗·🇺🇸 EN

Cómo probar componentes en Angular usando TestBed

Portada principal del artículo: Cómo probar componentes en Angular usando TestBed

⚡ ¿Usas Angular moderno (v17+ y Vitest)? Salta directamente a Pruebas modernas de componentes con Standalone, Signals y Vitest para ver la configuración de pruebas idiomática y ultrarrápida.

Cuando hablamos de pruebas unitarias, la definición clásica de libro de texto es probar las unidades aisladas más pequeñas de una aplicación, como funciones puras o clases aisladas. En teoría suena simple, pero en el desarrollo frontend del mundo real, los componentes tienen plantillas, inyección de dependencias, ciclos de vida, enlaces a signals y componentes hijos.

Si no sabes cómo configurar TestBed de forma adecuada, tus pruebas empezarán a fallar rápidamente con errores de proveedores faltantes (NullInjectorError) o de elementos desconocidos (NG0304).

En esta guía práctica, te mostraré cómo probar componentes en Angular paso a paso: cómo configurar TestBed, simular servicios inyectados, aislar componentes hijos de forma segura y migrar de configuraciones tradicionales con Jasmine y Karma al moderno Vitest.


El escenario: un componente de lista de jugadores de la NBA

Imaginemos que tenemos un componente que muestra una lista de jugadores de la NBA obtenidos desde una API a través de PlayerService. Este componente delega la renderización de cada tarjeta a un componente hijo <app-player>.

Aquí tienes la implementación del componente tanto en la sintaxis tradicional como en la moderna:

1. Angular tradicional (v2 a v16: NgModule e inyección por constructor)

// player-list.component.ts (Tradicional)
import { Component, OnInit } from '@angular/core';
import { PlayerService } from '../services/player.service';
import { Player } from '../models/player';
 
@Component({
  selector: 'app-player-list',
  templateUrl: './player-list.component.html',
})
export class PlayerListComponent implements OnInit {
  players: Player[] = [];
 
  constructor(private playerService: PlayerService) {}
 
  ngOnInit(): void {
    this.playerService.getPlayers().subscribe((res) => {
      this.players = res.data;
    });
  }
}
<!-- player-list.component.html (Tradicional) -->
<div *ngFor="let player of players" class="player-card">
  <app-player [player]="player"></app-player>
</div>

2. Angular moderno (v17+ / v19: Standalone, Signals e inject())

// player-list.component.ts (Moderno con Standalone y Signals)
import { Component, inject, OnInit, signal } from '@angular/core';
import { PlayerService } from '../services/player.service';
import { Player } from '../models/player';
import { PlayerComponent } from '../player/player.component';
 
@Component({
  selector: 'app-player-list',
  standalone: true,
  imports: [PlayerComponent],
  template: `
    @for (player of players(); track player.id) {
      <div class="player-card">
        <app-player [player]="player" />
      </div>
    } @empty {
      <p>No players found.</p>
    }
  `,
})
export class PlayerListComponent implements OnInit {
  private playerService = inject(PlayerService);
  players = signal<Player[]>([]);
 
  ngOnInit(): void {
    this.playerService.getPlayers().subscribe((res) => {
      this.players.set(res.data);
    });
  }
}

Ahora veamos cómo configuramos TestBed para probar este componente sin hacer peticiones HTTP reales ni romper las plantillas de los componentes hijos.


¿Qué es TestBed y cómo funciona?

TestBed es la API principal de pruebas en Angular. Crea un módulo de pruebas dinámico (DynamicTestModule) que simula el entorno de Angular en memoria.

El flujo de trabajo básico consta de dos fases:

  1. TestBed.configureTestingModule(...): Configura los imports, los providers y el componente bajo prueba.
  2. TestBed.createComponent(Component): Instancia el componente y devuelve un ComponentFixture.

El ComponentFixture te da acceso a:

  • fixture.componentInstance: La instancia de la clase TypeScript del componente (para verificar propiedades, signals y métodos).
  • fixture.nativeElement: El elemento DOM HTML raíz del componente.
  • fixture.debugElement: La abstracción de Angular sobre el DOM con utilidades de búsqueda (query(By.css(...))).
  • fixture.detectChanges(): Ejecuta manualmente la detección de cambios y procesa los enlaces de la plantilla.

Con estos conceptos claros, veamos cómo se estructura una suite de pruebas tradicional.


Paso a paso: pruebas con Jasmine y Karma tradicionales

Revisemos primero la configuración tradicional con Jasmine para que entiendas cómo ocurren los errores de inyección de dependencias.

Paso 1: Configuración básica de TestBed

// player-list.component.spec.ts (Jasmine)
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { PlayerListComponent } from './player-list.component';
import { PlayerService } from '../services/player.service';
 
describe('PlayerListComponent (Jasmine)', () => {
  let fixture: ComponentFixture<PlayerListComponent>;
  let component: PlayerListComponent;
 
  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [PlayerListComponent],
      providers: [
        // Si no proveemos PlayerService aquí, obtenemos NullInjectorError
      ],
    }).compileComponents();
 
    fixture = TestBed.createComponent(PlayerListComponent);
    component = fixture.componentInstance;
  });
 
  it('should create', () => {
    expect(component).toBeTruthy();
  });
});

Si ejecutas esta prueba, fallará con:

NullInjectorError: No provider for PlayerService!

Paso 2: Proveer un servicio simulado con espías de Jasmine

Para evitar que Angular cree el servicio real (el cual intentaría realizar llamadas HTTP reales), proveemos un mock usando jasmine.createSpyObj:

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { of } from 'rxjs';
import { PlayerListComponent } from './player-list.component';
import { PlayerService } from '../services/player.service';
 
describe('PlayerListComponent with Mock Service', () => {
  let fixture: ComponentFixture<PlayerListComponent>;
  let mockPlayerService: jasmine.SpyObj<PlayerService>;
 
  const FAKE_PLAYERS = {
    data: [
      { id: 1, name: 'LeBron James' },
      { id: 2, name: 'Stephen Curry' },
    ],
  };
 
  beforeEach(async () => {
    mockPlayerService = jasmine.createSpyObj<PlayerService>('PlayerService', ['getPlayers']);
    mockPlayerService.getPlayers.and.returnValue(of(FAKE_PLAYERS));
 
    await TestBed.configureTestingModule({
      declarations: [PlayerListComponent],
      providers: [
        { provide: PlayerService, useValue: mockPlayerService },
      ],
    }).compileComponents();
 
    fixture = TestBed.createComponent(PlayerListComponent);
  });
 
  it('should fetch players on ngOnInit', () => {
    fixture.detectChanges(); // Ejecuta ngOnInit()
 
    expect(mockPlayerService.getPlayers).toHaveBeenCalledTimes(1);
    expect(fixture.componentInstance.players.length).toBe(2);
  });
});

Paso 3: Manejar el componente hijo (<app-player>)

Cuando ejecutas fixture.detectChanges(), Angular analiza la plantilla HTML y encuentra <app-player>. Si este no está declarado en el módulo de pruebas, verás el siguiente error:

Error: NG0304: 'app-player' is not a known element

Nunca uses NO_ERRORS_SCHEMA para solucionar esto. Desactiva por completo la validación de la plantilla y oculta errores tipográficos reales. En su lugar, declara un componente stub ligero:

import { Component, Input } from '@angular/core';
import { By } from '@angular/platform-browser';
 
// 1. Componente stub ligero
@Component({
  selector: 'app-player',
  template: '<div class="player-stub">{{ player?.name }}</div>',
})
class MockPlayerComponent {
  @Input() player: any;
}
 
// 2. Agregar a declarations
beforeEach(async () => {
  await TestBed.configureTestingModule({
    declarations: [PlayerListComponent, MockPlayerComponent],
    providers: [{ provide: PlayerService, useValue: mockPlayerService }],
  }).compileComponents();
});
 
it('should render two player cards in the DOM', () => {
  fixture.detectChanges();
 
  const renderedPlayers = fixture.debugElement.queryAll(By.css('.player-stub'));
  expect(renderedPlayers.length).toBe(2);
});

Una vez resuelto el aislamiento de componentes en el entorno clásico, es momento de ver cómo la arquitectura moderna simplifica todo este proceso.


Pruebas modernas de componentes con Standalone, Signals y Vitest

En las versiones modernas de Angular (v18 y v19+), Vitest tiene soporte nativo a través del builder de pruebas de @angular/build. Vitest ejecuta las pruebas directamente en Node con ESM sin necesidad de levantar una pesada instancia de navegador Chrome, lo que permite que tus suites de pruebas se ejecuten en fracciones de segundo.

Aquí te muestro cómo probar un componente Standalone moderno con Signals usando Vitest:

// player-list.component.spec.ts (Angular moderno + Vitest)
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Component, input } from '@angular/core';
import { By } from '@angular/platform-browser';
import { of } from 'rxjs';
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { PlayerListComponent } from './player-list.component';
import { PlayerService } from '../services/player.service';
import { Player } from '../models/player';
 
// 1. Componente mock Standalone moderno usando signal inputs
@Component({
  selector: 'app-player',
  standalone: true,
  template: '<div class="player-stub">{{ player().name }}</div>',
})
class MockPlayerComponent {
  player = input.required<Player>();
}
 
describe('PlayerListComponent (Vitest + Standalone)', () => {
  let fixture: ComponentFixture<PlayerListComponent>;
  let component: PlayerListComponent;
 
  // 2. Mock del servicio usando vi.fn() de Vitest
  const mockPlayerService = {
    getPlayers: vi.fn(),
  };
 
  const MOCK_DATA = {
    data: [
      { id: 1, name: 'Luka Dončić' },
      { id: 2, name: 'Giannis Antetokounmpo' },
    ],
  };
 
  beforeEach(async () => {
    vi.clearAllMocks();
    mockPlayerService.getPlayers.mockReturnValue(of(MOCK_DATA));
 
    await TestBed.configureTestingModule({
      // Los componentes standalone van en 'imports', NO en 'declarations'
      imports: [PlayerListComponent],
      providers: [
        { provide: PlayerService, useValue: mockPlayerService },
      ],
    })
    // Reemplazamos el componente hijo real con nuestro mock
    .overrideComponent(PlayerListComponent, {
      remove: { imports: [] },
      add: { imports: [MockPlayerComponent] },
    })
    .compileComponents();
 
    fixture = TestBed.createComponent(PlayerListComponent);
    component = fixture.componentInstance;
  });
 
  it('should instantiate the standalone component', () => {
    expect(component).toBeTruthy();
  });
 
  it('should populate the players signal on init', () => {
    fixture.detectChanges();
 
    expect(mockPlayerService.getPlayers).toHaveBeenCalledTimes(1);
    // Verificamos los valores del signal directamente en las aserciones
    expect(component.players().length).toBe(2);
    expect(component.players()[0].name).toBe('Luka Dončić');
  });
 
  it('should render the list of players in the DOM with @for', () => {
    fixture.detectChanges();
 
    const playerNodes = fixture.debugElement.queryAll(By.css('.player-stub'));
    expect(playerNodes.length).toBe(2);
    expect(playerNodes[0].nativeElement.textContent).toContain('Luka Dončić');
  });
});

Además de probar el componente principal, a menudo necesitamos verificar de forma aislada componentes que reciben datos mediante signals.


Cómo probar Signal Inputs (input() y model())

Si quieres probar un componente hijo que acepta un input() basado en Signal, ya no necesitas crear componentes contenedores complejos. Puedes actualizar los signal inputs directamente usando fixture.componentRef.setInput:

// player.component.spec.ts (Pruebas de Signal Inputs con Vitest)
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { describe, it, expect, beforeEach } from 'vitest';
import { PlayerComponent } from './player.component';
 
describe('PlayerComponent (Signal Inputs)', () => {
  let fixture: ComponentFixture<PlayerComponent>;
  let component: PlayerComponent;
 
  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [PlayerComponent],
    }).compileComponents();
 
    fixture = TestBed.createComponent(PlayerComponent);
    component = fixture.componentInstance;
  });
 
  it('should render updated player details when input signal changes', () => {
    // Asignar el valor del signal input directamente
    fixture.componentRef.setInput('player', { id: 10, name: 'Nikola Jokić' });
    fixture.detectChanges();
 
    const nameElement = fixture.nativeElement.querySelector('.player-name');
    expect(nameElement.textContent).toContain('Nikola Jokić');
  });
});

Para ayudarte a tener a mano las diferencias clave entre ambos ecosistemas, preparé esta tabla de referencia rápida.


Tabla comparativa: Jasmine vs Vitest

CaracterísticaJasmine y Karma tradicionalVitest moderno
Motor de ejecuciónNavegador Chrome headless (inicio lento)Node.js nativo con ESM (ejecución en submilisegundos)
Funciones de mockjasmine.createSpy(), spyOn()vi.fn(), vi.spyOn()
Limpiar mocksjasmine.restoreAllMocks()vi.clearAllMocks(), vi.resetAllMocks()
Temporizadores asíncronosjasmine.clock().tick(100)vi.advanceTimersByTime(100)
Configuración Standalonedeclarations: [MyComp]imports: [MyComp]
Pruebas de inputscomponent.myInput = valfixture.componentRef.setInput('myInput', val)

Conclusión

Probar componentes de Angular con TestBed te da control total sobre la detección de cambios, la inyección de dependencias y las aserciones sobre el DOM.

Al migrar de las declaraciones tradicionales con NgModule y Jasmine/Karma hacia componentes Standalone y Vitest, eliminas la sobrecarga del navegador, obtienes retroalimentación instantánea en tu terminal y escribes pruebas limpias para tus signal inputs y el nuevo flujo de control.

Parte de la serie Angular

These are my experiences learning and facing my daily challenges working with Angular.

Ver serie completa

Preguntas Frecuentes

¿Cómo configuro TestBed para componentes Standalone en Angular?

Importa el componente standalone directamente en el arreglo imports de TestBed.configureTestingModule({ imports: [MyComponent], providers: [...] }) en lugar de declararlo.

¿Cómo se prueban componentes de Angular usando Vitest en lugar de Jasmine y Karma?

Angular moderno soporta Vitest de forma nativa. Usa vi.fn() y vi.spyOn() para mocks de servicios, importa TestBed desde @angular/core/testing y ejecuta las pruebas de forma nativa en ESM sin necesidad de abrir un navegador headless.

¿Cómo simular componentes hijos al probar un componente de Angular?

Puedes crear un componente dummy Standalone ligero con el mismo selector y signals input(), o usar TestBed.overrideComponent() para reemplazar dependencias hijas sin recurrir a NO_ERRORS_SCHEMA.

¿Cómo probar Signal inputs y estado en componentes de Angular?

Usa fixture.componentRef.setInput('propName', value) para actualizar los signal inputs, llama a fixture.detectChanges() y verifica la salida en el DOM o los valores de los signals computados del componente.

Artículos Relacionados

Compartir este artículo

Si esta guía te resultó útil, compártela con tu equipo o con otros desarrolladores.


Software real. Lecciones reales.

Comparto las lecciones que aprendí por las malas, para que puedas evitarlas o estar preparado cuando ocurran.

User avatar
User avatar
User avatar
User avatar
+13K

Únete a 13,800+ desarrolladores y lectores.

Cero spam. Cancela tu suscripción en cualquier momento.