Testing de componentes sin dependencias

Intermedio
Angular
Angular
Actualizado: 24/09/2025

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 nativo
  • fixture.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 - 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 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.