Capítulo 16: Testes Automatizados de Componentes e Serviços com TestBed e Vitest

Especialização em Front-End com Angular • Angular 18+ • Signals, Standalone Architecture, RxJS, NgRx e SSR


🗺️ Mapa Conceitual da Suíte de Testes com TestBed e Vitest

flowchart TD
    TestFile["Arquivo de Teste (*.spec.ts)"] --> VitestRunner["Vitest Test Runner (Ultra-Rápido via ESM)"]

    subgraph TESTBED_ENV["Ambiente Simulado do Angular (TestBed)"]
        TB["TestBed.configureTestingModule()"]
        PROV["provideHttpClientTesting() / Mocks de Serviços"]
        COMP["ComponentFixture<T>"]
    end

    VitestRunner --> TESTBED_ENV

    COMP --> DOM["fixture.nativeElement / debugElement (Inspeção de DOM)"]
    COMP --> INST["fixture.componentInstance (Estado TypeScript / Signals)"]
    COMP --> SET["fixture.componentRef.setInput('prop', valor)"]

    DOM & INST & SET --> Assertions["Asserções: expect().toBe() / expect().toContain()"]

    style TestFile fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
    style VitestRunner fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
    style TESTBED_ENV fill:#fff3e0,stroke:#ff9800,stroke-width:2px
    style Assertions fill:#e8f5e9,stroke:#4caf50,stroke-width:2px

🏛️ 1. A Nova Era de Testes no Angular: Vitest e TestBed Standalone

Historicamente, o ecossistema Angular dependia da dupla Karma e Jasmine para execução de testes unitários. No entanto, o Karma foi descontinuado pela comunidade devido à sua lentidão e necessidade de instanciar instâncias reais de navegadores para cada bateria de testes.

No Angular moderno, a comunidade e a engenharia adotaram o Vitest e o Jest sobre arquitetura ESM nativa:

  • Execução Instantânea: Dezenas de vezes mais rápida que o Karma, aproveitando compilação sob demanda via Vite / esbuild.
  • TestBed Otimizado: Com componentes Standalone, o TestBed não precisa declarar módulos gigantescos. Você importa apenas o componente sob teste e os mocks necessários.
  • Suporte Nativo a Signals: APIs dedicadas como fixture.componentRef.setInput() simplificam testes de componentes reativos.

⚙️ 2. Testando Componentes Baseados em Signals

Veja o teste completo de um componente Standalone que utiliza input(), computed() e emite eventos:

// contador-badge.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { describe, it, expect, beforeEach } from 'vitest';
import { ContadorBadgeComponent } from './contador-badge.component';

describe('ContadorBadgeComponent', () => {
  let component: ContadorBadgeComponent;
  let fixture: ComponentFixture<ContadorBadgeComponent>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [ContadorBadgeComponent] // Componente Standalone importado diretamente
    }).compileComponents();

    fixture = TestBed.createComponent(ContadorBadgeComponent);
    component = fixture.componentInstance;
  });

  it('deve instanciar o componente com sucesso', () => {
    expect(component).toBeTruthy();
  });

  it('deve atualizar a UI reativamente quando o Signal Input mudar', () => {
    // 1. Atualiza o input usando a nova API de teste de signals
    fixture.componentRef.setInput('totalNotificacoes', 5);
    
    // 2. Dispara a detecção de mudanças
    fixture.detectChanges();

    // 3. Inspeciona o elemento renderizado no DOM
    const badgeEl = fixture.debugElement.query(By.css('.badge-numero')).nativeElement;
    expect(badgeEl.textContent.trim()).toBe('5');
  });

  it('deve emitir o evento ao clicar no botão de reset', () => {
    let eventoDisparado = false;
    // Subscreve no Output funcional
    component.aoZerar.subscribe(() => {
      eventoDisparado = true;
    });

    const botao = fixture.debugElement.query(By.css('button.btn-reset')).nativeElement;
    botao.click();

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

📡 3. Testando Serviços com HttpTestingController

Para testar serviços que realizam requisições HTTP sem disparar chamadas de rede reais, utiliza-se o provideHttpClientTesting():

// cliente.service.spec.ts
import { TestBed } from '@angular/core/testing';
import { provideHttpClient } from '@angular/common/http';
import { provideHttpClientTesting, HttpTestingController } from '@angular/common/http/testing';
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { ClienteService } from './cliente.service';

describe('ClienteService', () => {
  let service: ClienteService;
  let httpMock: HttpTestingController;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        ClienteService,
        provideHttpClient(),
        provideHttpClientTesting()
      ]
    });

    service = TestBed.inject(ClienteService);
    httpMock = TestBed.inject(HttpTestingController);
  });

  afterEach(() => {
    // Garante que não ficaram requisições pendentes sem resposta
    httpMock.verify();
  });

  it('deve buscar clientes da API via GET', () => {
    const mockClientes = [
      { id: 1, nome: 'Empresa Alfa' },
      { id: 2, nome: 'Empresa Beta' }
    ];

    service.listarClientes().subscribe(clientes => {
      expect(clientes.length).toBe(2);
      expect(clientes[0].nome).toBe('Empresa Alfa');
    });

    // Intercepta a requisição esperada
    const req = httpMock.expectOne('/api/clientes');
    expect(req.request.method).toBe('GET');

    // Responde com dados falsos simulados
    req.flush(mockClientes);
  });
});

⚡ 4. Testes Unitários Puros: Zero Sobrecarga de TestBed

Nem todo teste exige instanciar o TestBed. Para Pipes, Services utilitários e Funções de validação, teste diretamente instanciando a classe com new, garantindo execução em menos de 2 milissegundos:

// cpf-mask.pipe.spec.ts
import { describe, it, expect } from 'vitest';
import { CpfMaskPipe } from './cpf-mask.pipe';

describe('CpfMaskPipe (Teste Unitário Puro)', () => {
  const pipe = new CpfMaskPipe();

  it('deve formatar 11 dígitos no padrão CPF', () => {
    const resultado = pipe.transform('12345678901');
    expect(resultado).toBe('123.456.789-01');
  });

  it('deve retornar string vazia se o valor for nulo', () => {
    expect(pipe.transform(null)).toBe('');
  });
});

⚖️ 5. Comparativo: Karma/Jasmine vs Vitest

Dimensão Técnica Karma + Jasmine (Legado) Vitest + Angular 18+ (Moderno)
Tempo de Inicialização 5 a 15 segundos (Abre browser Chrome) < 300 milissegundos (JSDOM / Happy-DOM)
Hot Module Reloading (Watch) Lento, recompila bundles pesados Instantâneo via ESM HMR
Suporte a ESM Limitado / Requer Webpack Nativo de primeira classe
Consumo de Memória Alto (Processo Chrome + Node) Mínimo (Thread isolada)
Status Oficial Descontinuado pela equipe web Recomendado pela indústria

⚠️ 6. Armadilhas Comuns e Anti-Patterns

  1. Esquecer fixture.detectChanges():
    • Erro: Modificar uma propriedade do componente e tentar verificar o DOM imediatamente. O DOM não atualizará sem a chamada explícita de fixture.detectChanges().
  2. Não chamar httpMock.verify() no afterEach:
    • Deixar requisições HTTP sem flush() pode causar efeitos colaterais silenciosos em outros arquivos de teste que executarem posteriormente.

🛡️ 7. Boas Práticas e Qualidade Corporativa

  • Foque em Comportamento, Não em Detalhes de Implementação: Teste o que o usuário vê e interage no DOM (botões, mensagens de erro, textos) em vez de inspecionar variáveis privadas da classe.
  • Manter Cobertura em Regras Críticas: Priorize 100% de cobertura de testes em guardas funcionais, validadores de formulário e serviços contábeis ou de checkout.

📋 8. Cheat-Sheet: Resumo Executivo

// Configuração rápida
await TestBed.configureTestingModule({ imports: [MeuComp] }).compileComponents();
const fixture = TestBed.createComponent(MeuComp);

// Atualizar Signal Input
fixture.componentRef.setInput('titulo', 'Novo Título');
fixture.detectChanges();

// Mock HTTP
httpMock.expectOne('/api/dados').flush({ ok: true });