ComponentFixture y testing de templates
Angular proporciona un entorno de testing especializado que nos permite crear y manipular componentes de forma controlada durante las pruebas. El núcleo de este sistema es la clase ComponentFixture
, que actúa como un contenedor que envuelve nuestro componente y nos da acceso tanto a la instancia del componente como a su representación en el DOM.
¿Qué es ComponentFixture?
La ComponentFixture es una clase que Angular Testing Utilities proporciona para encapsular un componente durante las pruebas. Piensa en ella como un "laboratorio de testing" donde podemos:
- Acceder a la instancia del componente y sus propiedades
- Obtener el elemento DOM renderizado
- Controlar cuándo se ejecuta la detección de cambios
- Realizar consultas sobre el template renderizado
Cuando creamos una fixture, Angular monta el componente en un entorno de testing aislado, pero no lo renderiza automáticamente. Esto nos da control total sobre cuándo y cómo se actualiza la vista.
Configuración básica con TestBed
Para crear una ComponentFixture, utilizamos TestBed.createComponent()
después de configurar el módulo de testing:
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { GreetingComponent } from './greeting.component';
describe('GreetingComponent', () => {
let component: GreetingComponent;
let fixture: ComponentFixture<GreetingComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [GreetingComponent] // Componente standalone
}).compileComponents();
fixture = TestBed.createComponent(GreetingComponent);
component = fixture.componentInstance;
});
});
En este ejemplo, fixture
contiene toda la información del componente montado, mientras que component
nos da acceso directo a la instancia del componente.
El método detectChanges()
Una de las características más importantes del testing en Angular es que la detección de cambios no es automática durante las pruebas. Esto significa que después de modificar propiedades del componente, debemos llamar manualmente a fixture.detectChanges()
para que los cambios se reflejen en el template.
it('should display the correct greeting', () => {
// Modificamos una propiedad del componente
component.name = 'Angular Developer';
// Sin detectChanges(), el template no se actualiza
expect(fixture.nativeElement.textContent).not.toContain('Angular Developer');
// Ejecutamos la detección de cambios
fixture.detectChanges();
// Ahora el template refleja el cambio
expect(fixture.nativeElement.textContent).toContain('Angular Developer');
});
Este control manual nos permite probar estados específicos del componente y verificar exactamente cuándo y cómo se producen los cambios en la vista.
Accediendo al DOM con debugElement
ComponentFixture nos proporciona dos formas principales de acceder al DOM renderizado:
fixture.nativeElement
: Acceso directo al elemento DOM nativofixture.debugElement
: Wrapper de Angular que proporciona utilidades adicionales
Para consultas básicas en templates, debugElement
ofrece métodos más convenientes:
import { By } from '@angular/platform-browser';
it('should render the title in an h1 tag', () => {
component.title = 'Welcome to Angular Testing';
fixture.detectChanges();
// Buscar por selector CSS
const titleElement = fixture.debugElement.query(By.css('h1'));
expect(titleElement.nativeElement.textContent).toContain('Welcome to Angular Testing');
// Alternativa con nativeElement directo
const h1 = fixture.nativeElement.querySelector('h1');
expect(h1.textContent).toContain('Welcome to Angular Testing');
});
Ejemplo práctico: Testing de un componente simple
Consideremos un componente standalone básico que muestra información del usuario:
// user-card.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-user-card',
standalone: true,
template: `
<div class="user-card">
<h2>{{ userName }}</h2>
@if (showEmail) {
<p class="email">{{ email }}</p>
}
<span class="status" [class.active]="isActive">
{{ isActive ? 'Active' : 'Inactive' }}
</span>
</div>
`,
styles: [`
.user-card { padding: 1rem; border: 1px solid #ccc; }
.status.active { color: green; }
.email { font-style: italic; }
`]
})
export class UserCardComponent {
userName = '';
email = '';
showEmail = false;
isActive = false;
}
Las pruebas correspondientes verifican cómo se renderiza el template:
describe('UserCardComponent Template', () => {
let component: UserCardComponent;
let fixture: ComponentFixture<UserCardComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [UserCardComponent]
}).compileComponents();
fixture = TestBed.createComponent(UserCardComponent);
component = fixture.componentInstance;
});
it('should render the user name in h2', () => {
component.userName = 'John Doe';
fixture.detectChanges();
const h2Element = fixture.debugElement.query(By.css('h2'));
expect(h2Element.nativeElement.textContent).toBe('John Doe');
});
it('should show email when showEmail is true', () => {
component.email = 'john@example.com';
component.showEmail = true;
fixture.detectChanges();
const emailElement = fixture.debugElement.query(By.css('.email'));
expect(emailElement).toBeTruthy();
expect(emailElement.nativeElement.textContent).toContain('john@example.com');
});
it('should hide email when showEmail is false', () => {
component.email = 'john@example.com';
component.showEmail = false;
fixture.detectChanges();
const emailElement = fixture.debugElement.query(By.css('.email'));
expect(emailElement).toBeNull();
});
it('should apply active class when user is active', () => {
component.isActive = true;
fixture.detectChanges();
const statusElement = fixture.debugElement.query(By.css('.status'));
expect(statusElement.nativeElement.classList).toContain('active');
expect(statusElement.nativeElement.textContent.trim()).toBe('Active');
});
});
Consultas múltiples con queryAll
Cuando necesitamos buscar múltiples elementos que coincidan con un selector, utilizamos queryAll()
:
it('should render multiple list items', () => {
component.items = ['Item 1', 'Item 2', 'Item 3'];
fixture.detectChanges();
const listItems = fixture.debugElement.queryAll(By.css('li'));
expect(listItems.length).toBe(3);
expect(listItems[0].nativeElement.textContent).toContain('Item 1');
expect(listItems[2].nativeElement.textContent).toContain('Item 3');
});
Mejores prácticas para ComponentFixture
Inicialización en beforeEach: Siempre crea la fixture en beforeEach
para asegurar un estado limpio en cada prueba.
DetectChanges explícito: Llama a detectChanges()
después de modificar propiedades del componente, no antes de verificar el DOM.
Consultas específicas: Usa selectores CSS específicos para evitar falsos positivos en las pruebas.
// Buena práctica: selector específico
const submitButton = fixture.debugElement.query(By.css('button[type="submit"]'));
// Evitar: selector genérico que podría coincidir con otros elementos
const button = fixture.debugElement.query(By.css('button'));
Verificación de existencia: Siempre verifica que el elemento existe antes de acceder a sus propiedades.
it('should display error message when present', () => {
component.errorMessage = 'Something went wrong';
fixture.detectChanges();
const errorElement = fixture.debugElement.query(By.css('.error'));
expect(errorElement).toBeTruthy(); // Verificar existencia
expect(errorElement.nativeElement.textContent).toContain('Something went wrong');
});
ComponentFixture y detectChanges()
forman la base fundamental del testing de componentes en Angular, proporcionando el control necesario para verificar que nuestros templates se renderizan correctamente bajo diferentes condiciones.
Testing básico de componentes
Una vez que dominamos ComponentFixture y la renderización de templates, el siguiente paso es probar la funcionalidad del componente. Esto incluye testing de inputs, outputs, métodos públicos e interacciones básicas del usuario. Nos enfocaremos en componentes sencillos sin dependencias externas para mantener las pruebas simples y directas.
Testing de propiedades de entrada (inputs)
Los inputs del componente son propiedades que reciben datos desde componentes padre. Probar estos inputs implica verificar que el componente reacciona correctamente cuando recibe diferentes valores.
Consideremos un componente contador básico:
// counter.component.ts
import { Component, input } from '@angular/core';
@Component({
selector: 'app-counter',
standalone: true,
template: `
<div class="counter">
<h3>Contador: {{ currentValue() }}</h3>
<p>Valor inicial: {{ initialValue() }}</p>
<button (click)="increment()" class="btn-increment">+1</button>
<button (click)="decrement()" class="btn-decrement">-1</button>
<button (click)="reset()" class="btn-reset">Reset</button>
</div>
`
})
export class CounterComponent {
initialValue = input<number>(0);
currentValue = input<number>(0);
increment() {
// Lógica simple sin signals por simplicidad
this.currentValue.set(this.currentValue() + 1);
}
decrement() {
this.currentValue.set(this.currentValue() - 1);
}
reset() {
this.currentValue.set(this.initialValue());
}
}
Las pruebas para los inputs verifican que se muestran correctamente:
describe('CounterComponent Inputs', () => {
let component: CounterComponent;
let fixture: ComponentFixture<CounterComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [CounterComponent]
}).compileComponents();
fixture = TestBed.createComponent(CounterComponent);
component = fixture.componentInstance;
});
it('should display initial value correctly', () => {
fixture.componentRef.setInput('initialValue', 10);
fixture.componentRef.setInput('currentValue', 10);
fixture.detectChanges();
const initialValueElement = fixture.debugElement.query(By.css('p'));
expect(initialValueElement.nativeElement.textContent).toContain('Valor inicial: 10');
});
it('should display current value correctly', () => {
fixture.componentRef.setInput('currentValue', 25);
fixture.detectChanges();
const currentValueElement = fixture.debugElement.query(By.css('h3'));
expect(currentValueElement.nativeElement.textContent).toContain('Contador: 25');
});
});
Testing de eventos de salida (outputs)
Los outputs del componente emiten eventos hacia componentes padre. Para probarlos, utilizamos espías (spies) que nos permiten verificar que los eventos se emiten con los datos correctos.
Creemos un componente de notificación simple:
// notification.component.ts
import { Component, output } from '@angular/core';
@Component({
selector: 'app-notification',
standalone: true,
template: `
<div class="notification" [class.visible]="isVisible">
<p>{{ message }}</p>
<button (click)="dismiss()" class="btn-close">×</button>
<button (click)="snooze()" class="btn-snooze">Posponer</button>
</div>
`
})
export class NotificationComponent {
message = 'Notificación importante';
isVisible = true;
dismissed = output<string>();
snoozed = output<{ message: string; minutes: number }>();
dismiss() {
this.isVisible = false;
this.dismissed.emit(this.message);
}
snooze() {
this.isVisible = false;
this.snoozed.emit({
message: this.message,
minutes: 5
});
}
}
Para probar los outputs, creamos espías que capturen las emisiones:
describe('NotificationComponent Outputs', () => {
let component: NotificationComponent;
let fixture: ComponentFixture<NotificationComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [NotificationComponent]
}).compileComponents();
fixture = TestBed.createComponent(NotificationComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should emit dismissed event when close button is clicked', () => {
// Creamos un espía para capturar el output
spyOn(component.dismissed, 'emit');
const closeButton = fixture.debugElement.query(By.css('.btn-close'));
closeButton.nativeElement.click();
expect(component.dismissed.emit).toHaveBeenCalledWith('Notificación importante');
expect(component.isVisible).toBeFalse();
});
it('should emit snoozed event with correct data', () => {
spyOn(component.snoozed, 'emit');
const snoozeButton = fixture.debugElement.query(By.css('.btn-snooze'));
snoozeButton.nativeElement.click();
expect(component.snoozed.emit).toHaveBeenCalledWith({
message: 'Notificación importante',
minutes: 5
});
});
});
Testing de interacciones de usuario
Las interacciones básicas del usuario como clicks, cambios en inputs y envío de formularios son fundamentales para probar. Angular testing nos permite simular estas interacciones directamente en el DOM.
Ejemplo con un formulario básico:
// contact-form.component.ts
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-contact-form',
standalone: true,
imports: [FormsModule],
template: `
<form (ngSubmit)="onSubmit()" class="contact-form">
<input
[(ngModel)]="name"
name="name"
placeholder="Tu nombre"
class="input-name">
<input
[(ngModel)]="email"
name="email"
type="email"
placeholder="Tu email"
class="input-email">
<button type="submit" [disabled]="!isFormValid()" class="btn-submit">
Enviar
</button>
</form>
@if (submitted) {
<div class="success-message">¡Formulario enviado correctamente!</div>
}
`
})
export class ContactFormComponent {
name = '';
email = '';
submitted = false;
isFormValid(): boolean {
return this.name.trim().length > 0 && this.email.includes('@');
}
onSubmit() {
if (this.isFormValid()) {
this.submitted = true;
// Aquí normalmente enviaríamos los datos
console.log('Enviando:', { name: this.name, email: this.email });
}
}
}
Las pruebas de interacciones simulan la entrada del usuario:
describe('ContactFormComponent Interactions', () => {
let component: ContactFormComponent;
let fixture: ComponentFixture<ContactFormComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ContactFormComponent]
}).compileComponents();
fixture = TestBed.createComponent(ContactFormComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should enable submit button when form is valid', () => {
const nameInput = fixture.debugElement.query(By.css('.input-name'));
const emailInput = fixture.debugElement.query(By.css('.input-email'));
const submitButton = fixture.debugElement.query(By.css('.btn-submit'));
// Simulamos entrada de datos del usuario
nameInput.nativeElement.value = 'Juan Pérez';
nameInput.nativeElement.dispatchEvent(new Event('input'));
emailInput.nativeElement.value = 'juan@example.com';
emailInput.nativeElement.dispatchEvent(new Event('input'));
fixture.detectChanges();
expect(component.name).toBe('Juan Pérez');
expect(component.email).toBe('juan@example.com');
expect(submitButton.nativeElement.disabled).toBeFalse();
});
it('should show success message after form submission', () => {
// Configuramos datos válidos
component.name = 'María García';
component.email = 'maria@test.com';
fixture.detectChanges();
const form = fixture.debugElement.query(By.css('.contact-form'));
form.nativeElement.dispatchEvent(new Event('submit'));
fixture.detectChanges();
const successMessage = fixture.debugElement.query(By.css('.success-message'));
expect(successMessage).toBeTruthy();
expect(successMessage.nativeElement.textContent).toContain('¡Formulario enviado correctamente!');
expect(component.submitted).toBeTrue();
});
});
Testing de métodos del componente
Además de probar la interfaz de usuario, también debemos verificar la lógica interna del componente. Esto incluye métodos públicos, cálculos y transformaciones de datos.
describe('ContactFormComponent Methods', () => {
let component: ContactFormComponent;
let fixture: ComponentFixture<ContactFormComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ContactFormComponent]
}).compileComponents();
fixture = TestBed.createComponent(ContactFormComponent);
component = fixture.componentInstance;
});
it('should validate form correctly', () => {
// Formulario vacío debe ser inválido
component.name = '';
component.email = '';
expect(component.isFormValid()).toBeFalse();
// Solo nombre debe ser inválido
component.name = 'Juan';
component.email = '';
expect(component.isFormValid()).toBeFalse();
// Email inválido debe ser inválido
component.name = 'Juan';
component.email = 'email-invalido';
expect(component.isFormValid()).toBeFalse();
// Datos válidos debe ser válido
component.name = 'Juan';
component.email = 'juan@example.com';
expect(component.isFormValid()).toBeTrue();
});
it('should handle form submission correctly', () => {
spyOn(console, 'log'); // Espía para verificar console.log
component.name = 'Ana López';
component.email = 'ana@test.com';
component.onSubmit();
expect(component.submitted).toBeTrue();
expect(console.log).toHaveBeenCalledWith('Enviando:', {
name: 'Ana López',
email: 'ana@test.com'
});
});
});
Assertions básicas más utilizadas
En las pruebas de componentes, utilizamos diferentes tipos de assertions según lo que queramos verificar:
Para valores booleanos:
expect(component.isVisible).toBeTrue();
expect(component.isDisabled).toBeFalse();
expect(component.hasErrors).toBeTruthy(); // Cualquier valor "truthy"
expect(component.data).toBeFalsy(); // Cualquier valor "falsy"
Para contenido de texto:
expect(element.nativeElement.textContent).toBe('Texto exacto');
expect(element.nativeElement.textContent).toContain('parte del texto');
expect(component.message).toEqual('Mensaje esperado');
Para arrays y objetos:
expect(component.items.length).toBe(3);
expect(component.user).toEqual({ name: 'Juan', age: 30 });
expect(component.tags).toContain('angular');
Para elementos DOM:
expect(element).toBeTruthy(); // El elemento existe
expect(element).toBeNull(); // El elemento no existe
expect(element.nativeElement.classList).toContain('active');
expect(element.nativeElement.disabled).toBeTruthy();
Ejemplo integrado: Testing completo de componente simple
Para consolidar estos conceptos, veamos un ejemplo completo de un componente de votación:
// vote-component.ts
import { Component, input, output } from '@angular/core';
@Component({
selector: 'app-vote',
standalone: true,
template: `
<div class="vote-widget">
<h4>{{ title() }}</h4>
<div class="vote-buttons">
<button (click)="vote('up')" class="btn-up" [disabled]="hasVoted">
👍 {{ upVotes }}
</button>
<button (click)="vote('down')" class="btn-down" [disabled]="hasVoted">
👎 {{ downVotes }}
</button>
</div>
@if (hasVoted) {
<p class="voted-message">¡Gracias por votar!</p>
}
</div>
`
})
export class VoteComponent {
title = input<string>('¿Te gusta este contenido?');
upVotes = 0;
downVotes = 0;
hasVoted = false;
voted = output<{ type: 'up' | 'down'; total: number }>();
vote(type: 'up' | 'down') {
if (this.hasVoted) return;
if (type === 'up') {
this.upVotes++;
} else {
this.downVotes++;
}
this.hasVoted = true;
this.voted.emit({ type, total: this.upVotes + this.downVotes });
}
}
Test completo del componente:
describe('VoteComponent Complete Testing', () => {
let component: VoteComponent;
let fixture: ComponentFixture<VoteComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [VoteComponent]
}).compileComponents();
fixture = TestBed.createComponent(VoteComponent);
component = fixture.componentInstance;
});
it('should display custom title', () => {
fixture.componentRef.setInput('title', 'Califica este artículo');
fixture.detectChanges();
const titleElement = fixture.debugElement.query(By.css('h4'));
expect(titleElement.nativeElement.textContent).toBe('Califica este artículo');
});
it('should handle up vote correctly', () => {
spyOn(component.voted, 'emit');
fixture.detectChanges();
const upButton = fixture.debugElement.query(By.css('.btn-up'));
upButton.nativeElement.click();
fixture.detectChanges();
expect(component.upVotes).toBe(1);
expect(component.hasVoted).toBeTrue();
expect(component.voted.emit).toHaveBeenCalledWith({ type: 'up', total: 1 });
const votedMessage = fixture.debugElement.query(By.css('.voted-message'));
expect(votedMessage).toBeTruthy();
});
it('should disable buttons after voting', () => {
fixture.detectChanges();
const upButton = fixture.debugElement.query(By.css('.btn-up'));
const downButton = fixture.debugElement.query(By.css('.btn-down'));
// Inicialmente habilitados
expect(upButton.nativeElement.disabled).toBeFalse();
expect(downButton.nativeElement.disabled).toBeFalse();
// Después de votar, deshabilitados
upButton.nativeElement.click();
fixture.detectChanges();
expect(upButton.nativeElement.disabled).toBeTrue();
expect(downButton.nativeElement.disabled).toBeTrue();
});
it('should prevent double voting', () => {
spyOn(component.voted, 'emit');
fixture.detectChanges();
const upButton = fixture.debugElement.query(By.css('.btn-up'));
// Primer voto
upButton.nativeElement.click();
// Intento de segundo voto
upButton.nativeElement.click();
expect(component.upVotes).toBe(1); // Solo un voto contado
expect(component.voted.emit).toHaveBeenCalledTimes(1); // Solo una emisión
});
});
Este enfoque de testing básico nos permite verificar que nuestros componentes funcionan correctamente sin necesidad de dependencias complejas o configuraciones avanzadas. La clave está en mantener los componentes simples y las pruebas enfocadas en comportamientos específicos y verificables.

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 uso de ComponentFixture para testear componentes y sus templates.
- Aprender a controlar la detección de cambios manualmente con detectChanges().
- Saber cómo probar inputs y outputs de componentes standalone.
- Realizar pruebas de interacciones básicas de usuario en componentes.
- Verificar la lógica interna mediante tests de métodos públicos del componente.