Especialização em Front-End com Angular • Demonstrações Práticas Compiláveis


💻 Código de Exemplo 16 — Testes Unitários de Componentes Standalone com TestBed e Vitest

Este exemplo apresenta a criação de uma suíte de testes unitários para componentes Angular 18 baseados em Signals utilizando TestBed, ComponentFixture e o executor moderno Vitest, validando renderização de inputs, simulação de eventos e asserções no DOM.

1. Componente Alvo: src/app/components/quota-badge.component.ts

// src/app/components/quota-badge.component.ts
import { Component, input, output, computed } from '@angular/core';

@Component({
  selector: 'app-quota-badge',
  standalone: true,
  template: `
    <div
      class="badge-shell flex items-center justify-between p-3 border rounded-xl"
      [class.border-red-500]="isExceeded()"
      data-testid="quota-container"
    >
      <div class="space-y-0.5">
        <span class="text-xs text-slate-500" data-testid="quota-label">{{ label() }}</span>
        <div class="text-sm font-bold text-slate-900" data-testid="quota-display">
          {{ current() }} / {{ limit() }} MB
        </div>
      </div>

      <button
        type="button"
        (click)="onResetClicked()"
        class="text-xs px-2.5 py-1 bg-slate-100 hover:bg-slate-200 text-slate-700 rounded-md font-semibold"
        data-testid="btn-reset"
      >
        Zerar
      </button>
    </div>
  `
})
export class QuotaBadgeComponent {
  label = input.required<string>();
  current = input<number>(0);
  limit = input<number>(100);

  reset = output<void>();

  isExceeded = computed(() => this.current() >= this.limit());

  onResetClicked(): void {
    this.reset.emit();
  }
}

2. Arquivo de Teste Automatizado: src/app/components/quota-badge.component.spec.ts

// src/app/components/quota-badge.component.spec.ts
import { describe, it, expect, beforeEach } from 'vitest';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentRef } from '@angular/core';
import { QuotaBadgeComponent } from './quota-badge.component';

describe('QuotaBadgeComponent (Angular 18 + Vitest)', () => {
  let fixture: ComponentFixture<QuotaBadgeComponent>;
  let component: QuotaBadgeComponent;
  let componentRef: ComponentRef<QuotaBadgeComponent>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [QuotaBadgeComponent]
    }).compileComponents();

    fixture = TestBed.createComponent(QuotaBadgeComponent);
    component = fixture.componentInstance;
    componentRef = fixture.componentRef;
  });

  it('deve renderizar o label e a quota atual corretamente via signal input', () => {
    // Definindo inputs tipadas através do setInput() do ComponentRef
    componentRef.setInput('label', 'Memória Redis');
    componentRef.setInput('current', 45);
    componentRef.setInput('limit', 100);

    // Dispara detecção de mudanças para atualizar o DOM
    fixture.detectChanges();

    const compiled = fixture.nativeElement as HTMLElement;
    const labelEl = compiled.querySelector('[data-testid="quota-label"]');
    const displayEl = compiled.querySelector('[data-testid="quota-display"]');

    expect(labelEl?.textContent?.trim()).toBe('Memória Redis');
    expect(displayEl?.textContent?.trim()).toBe('45 / 100 MB');
  });

  it('deve aplicar borda vermelha quando current() for maior ou igual ao limit()', () => {
    componentRef.setInput('label', 'Disco SAS');
    componentRef.setInput('current', 120);
    componentRef.setInput('limit', 100);

    fixture.detectChanges();

    const container = fixture.nativeElement.querySelector('[data-testid="quota-container"]') as HTMLElement;
    expect(container.classList.contains('border-red-500')).toBe(true);
  });

  it('deve emitir o signal output "reset" ao clicar no botão Zerar', () => {
    componentRef.setInput('label', 'CPU Pool');

    let wasResetEmitted = false;
    // Subscrevendo ao signal output funcional
    component.reset.subscribe(() => {
      wasResetEmitted = true;
    });

    fixture.detectChanges();

    const button = fixture.nativeElement.querySelector('[data-testid="btn-reset"]') as HTMLButtonElement;
    button.click();

    expect(wasResetEmitted).toBe(true);
  });
});

🖥️ Saída / Comportamento Esperado

Execução do Vitest no terminal (npx vitest run):

 ✓ src/app/components/quota-badge.component.spec.ts (3 tests) 94ms
   ✓ QuotaBadgeComponent (Angular 18 + Vitest) (3)
     ✓ deve renderizar o label e a quota atual corretamente via signal input
     ✓ deve aplicar borda vermelha quando current() for maior ou igual ao limit()
     ✓ deve emitir o signal output "reset" ao clicar no botão Zerar

 Test Files  1 passed (1)
      Tests  3 passed (3)
   Duration  380ms

🔍 Destaques da Implementação

  1. componentRef.setInput(): API oficial para atribuir valores a Signal Inputs em tempo de teste unitário.
  2. Subscrição Simples em Signal Output: component.reset.subscribe() permite validar a emissão de outputs com clareza.
  3. Seletores Desacoplados (data-testid): Garante resiliência de testes contra alterações de estilização Tailwind.