Ir para o conteúdo
Framework com Framework1547 palavras

Como usar Angular com Vitest: Guia Completo

Publicado no blog Vivo de Código

Introdução

Se você já trabalhou com Angular em projetos de médio ou grande porte, sabe que a suite de testes padrão baseada em Karma e Jasmine pode se tornar um gargalo real no ciclo de desenvolvimento. Builds lentos, configurações frágeis e um ecossistema que parou no tempo são queixas recorrentes. A boa notícia: existe uma alternativa madura, rápida e que se integra muito bem ao Angular. Estou falando do Vitest.

O Vitest é um framework de testes construído sobre o Vite, projetado para oferecer velocidade extrema e uma API compatível com Jest. A combinação Angular com Vitest tem ganhado tração na comunidade, especialmente depois que o próprio time do Angular passou a investir em builders baseados em ESBuild e a flexibilizar o toolchain oficial.

Neste guia, vou mostrar passo a passo como configurar o Vitest em um projeto Angular, com exemplos práticos de testes de componente e de serviço. Ao final, você terá uma configuração funcional e um conjunto de boas práticas para aplicar no seu dia a dia.

Por que usar Angular com Vitest

O problema com Karma e Jasmine

O Karma foi, durante anos, o test runner padrão do Angular. Ele funciona abrindo um navegador real (ou headless) para executar os testes, o que traz algumas consequências:

  1. O tempo de inicialização é alto, mesmo para poucos testes.
  2. A configuração do karma.conf.js junto com o angular.json é verbosa e propensa a erros.
  3. O ecossistema estagnou. O Karma foi oficialmente descontinuado em 2023.

O time do Angular reconheceu essa limitação e, a partir da versão 16, começou a experimentar com Jest e Web Test Runner como alternativas. Mas o Vitest oferece vantagens que vão além dessas opções.

O que o Vitest traz de diferente

O Vitest resolve vários dos problemas mencionados acima de forma elegante:

  • Velocidade: por rodar sobre o Vite, o Vitest usa transformação sob demanda com ESBuild, o que elimina a necessidade de compilar todo o projeto antes de rodar testes.
  • API familiar: a API é praticamente idêntica à do Jest. Se você já escreveu describe, it, expect e vi.fn(), está em casa.
  • Hot Module Replacement para testes: o Vitest re-executa apenas os testes afetados por mudanças no código, tornando o ciclo de feedback quase instantâneo.
  • Suporte nativo a TypeScript e ESModules: sem necessidade de configurações extras para transpilação.
  • Ecossistema ativo: o Vitest tem uma comunidade vibrante e atualizações frequentes.

Casos de uso reais onde essa combinação brilha

Em projetos onde trabalhei, a migração de Karma para Vitest trouxe resultados expressivos. Um monorepo com cerca de 800 testes unitários que levava 4 minutos para rodar com Karma passou a executar em menos de 40 segundos com Vitest. Outro cenário comum é o de equipes que já usam Vite como bundler no Angular (via @analogjs/vite-angular ou o builder experimental do Angular) e querem unificar o toolchain. Nesse contexto, o Vitest é a escolha natural.

Setup completo do Vitest em um projeto Angular

Pré-requisitos

Vou considerar que você tem um projeto Angular na versão 15 ou superior. O processo funciona melhor a partir da versão 17, onde o suporte a ESBuild é mais maduro.

Passo 1: instalar as dependências

TYPESCRIPT
// Execute no terminal:
// npm install -D vitest @analogjs/vite-angular jsdom

O pacote @analogjs/vite-angular é o plugin que permite ao Vite (e, por consequência, ao Vitest) entender os arquivos do Angular, como componentes com templates e estilos inline ou externos. O jsdom será nosso ambiente de DOM simulado.

Passo 2: criar o arquivo de configuração do Vitest

Crie um arquivo vite.config.ts na raiz do projeto:

TYPESCRIPT
/// <reference types="vitest" />
import { defineConfig } from 'vite';
import angular from '@analogjs/vite-angular';
 
export default defineConfig({
  plugins: [angular()],
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: ['src/test-setup.ts'],
    include: ['src/**/*.spec.ts'],
    reporters: ['default'],
    css: true,
  },
});

Vamos entender cada opção relevante:

  • globals: true permite usar describe, it e expect sem importação explícita.
  • environment: 'jsdom' simula o DOM no Node.js.
  • setupFiles aponta para um arquivo de inicialização que veremos a seguir.
  • include define o padrão de busca para arquivos de teste.

Passo 3: criar o arquivo de setup

Crie o arquivo src/test-setup.ts:

TYPESCRIPT
import '@analogjs/vite-angular/setup-zone';
import { getTestBed } from '@angular/core/testing';
import {
  BrowserDynamicTestingModule,
  platformBrowserDynamicTesting,
} from '@angular/platform-browser-dynamic/testing';
 
getTestBed().initTestEnvironment(
  BrowserDynamicTestingModule,
  platformBrowserDynamicTesting()
);

Esse arquivo inicializa o TestBed do Angular, que é essencial para testes de componentes e serviços que dependem de injeção de dependência.

Passo 4: configurar o TypeScript

Atualize (ou crie) o arquivo tsconfig.spec.json para incluir os tipos do Vitest:

TYPESCRIPT
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "outDir": "./out-tsc/spec",
    "types": ["vitest/globals"]
  },
  "include": ["src/**/*.spec.ts", "src/**/*.d.ts", "src/test-setup.ts"]
}

Passo 5: adicionar o script no package.json

TYPESCRIPT
// No package.json, adicione:
{
  "scripts": {
    "test": "vitest",
    "test:run": "vitest run",
    "test:coverage": "vitest run --coverage"
  }
}

O comando vitest inicia o modo watch. O vitest run executa uma única vez (ideal para CI/CD).

Exemplo prático: testando um serviço Angular com Vitest

Vamos criar um serviço simples de autenticação e seus testes. Este é um cenário que você encontra em praticamente qualquer aplicação Angular.

O serviço

TYPESCRIPT
// src/app/services/auth.service.ts
import { Injectable, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, tap } from 'rxjs';
 
export interface LoginResponse {
  token: string;
  expiresIn: number;
}
 
@Injectable({
  providedIn: 'root',
})
export class AuthService {
  private readonly apiUrl = '/api/auth';
  isAuthenticated = signal(false);
 
  constructor(private http: HttpClient) {}
 
  login(email: string, password: string): Observable<LoginResponse> {
    return this.http
      .post<LoginResponse>(`${this.apiUrl}/login`, { email, password })
      .pipe(
        tap((response) => {
          localStorage.setItem('auth_token', response.token);
          this.isAuthenticated.set(true);
        })
      );
  }
 
  logout(): void {
    localStorage.removeItem('auth_token');
    this.isAuthenticated.set(false);
  }
 
  getToken(): string | null {
    return localStorage.getItem('auth_token');
  }
}

Os testes

TYPESCRIPT
// src/app/services/auth.service.spec.ts
import { TestBed } from '@angular/core/testing';
import {
  HttpClientTestingModule,
  HttpTestingController,
} from '@angular/common/http/testing';
import { AuthService, LoginResponse } from './auth.service';
 
describe('AuthService', () => {
  let service: AuthService;
  let httpMock: HttpTestingController;
 
  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
    });
 
    service = TestBed.inject(AuthService);
    httpMock = TestBed.inject(HttpTestingController);
    localStorage.clear();
  });
 
  afterEach(() => {
    httpMock.verify();
  });
 
  it('deve ser criado com estado não autenticado', () => {
    expect(service).toBeTruthy();
    expect(service.isAuthenticated()).toBe(false);
  });
 
  it('deve autenticar o usuário após login bem-sucedido', () => {
    const mockResponse: LoginResponse = {
      token: 'jwt-token-abc123',
      expiresIn: 3600,
    };
 
    service.login('[email protected]', 'senha123').subscribe((response) => {
      expect(response.token).toBe('jwt-token-abc123');
      expect(service.isAuthenticated()).toBe(true);
      expect(localStorage.getItem('auth_token')).toBe('jwt-token-abc123');
    });
 
    const req = httpMock.expectOne('/api/auth/login');
    expect(req.request.method).toBe('POST');
    expect(req.request.body).toEqual({
      email: '[email protected]',
      password: 'senha123',
    });
    req.flush(mockResponse);
  });
 
  it('deve limpar o token e atualizar o estado ao fazer logout', () => {
    localStorage.setItem('auth_token', 'token-existente');
    service.isAuthenticated.set(true);
 
    service.logout();
 
    expect(service.isAuthenticated()).toBe(false);
    expect(localStorage.getItem('auth_token')).toBeNull();
  });
 
  it('deve retornar null quando não há token armazenado', () => {
    expect(service.getToken()).toBeNull();
  });
 
  it('deve retornar o token quando ele existe no localStorage', () => {
    localStorage.setItem('auth_token', 'meu-token');
    expect(service.getToken()).toBe('meu-token');
  });
});

Exemplo prático: testando um componente Angular com Vitest

Agora vamos testar um componente que depende do serviço criado acima.

O componente

TYPESCRIPT
// src/app/components/user-greeting/user-greeting.component.ts
import { Component, Input, computed } from '@angular/core';
import { CommonModule } from '@angular/common';
import { AuthService } from '../../services/auth.service';
 
@Component({
  selector: 'app-user-greeting',
  standalone: true,
  imports: [CommonModule],
  template: `
    <div class="greeting-container">
      @if (isLoggedIn()) {
        <h2>Bem-vindo, {{ userName }}!</h2>
        <button (click)="onLogout()" data-testid="logout-btn">Sair</button>
      } @else {
        <p data-testid="guest-message">Faça login para continuar.</p>
      }
    </div>
  `,
})
export class UserGreetingComponent {
  @Input() userName = 'Visitante';
 
  isLoggedIn = computed(() => this.authService.isAuthenticated());
 
  constructor(private authService: AuthService) {}
 
  onLogout(): void {
    this.authService.logout();
  }
}

Os testes do componente

TYPESCRIPT
// src/app/components/user-greeting/user-greeting.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { UserGreetingComponent } from './user-greeting.component';
import { AuthService } from '../../services/auth.service';
import { signal } from '@angular/core';
 
describe('UserGreetingComponent', () => {
  let component: UserGreetingComponent;
  let fixture: ComponentFixture<UserGreetingComponent>;
  let mockAuthService: Partial<AuthService>;
  const isAuthenticatedSignal = signal(false);
 
  beforeEach(async () => {
    mockAuthService = {
      isAuthenticated: isAuthenticatedSignal,
      logout: vi.fn(() => {
        isAuthenticatedSignal.set(false);
      }),
    };
 
    await TestBed.configureTestingModule({
      imports: [UserGreetingComponent],
      providers: [{ provide: AuthService, useValue: mockAuthService }],
    }).compileComponents();
 
    fixture = TestBed.createComponent(UserGreetingComponent);
    component = fixture.componentInstance;
  });
 
  it('deve exibir mensagem para visitantes quando não autenticado', () => {
    isAuthenticatedSignal.set(false);
    fixture.detectChanges();
 
    const guestMessage =
      fixture.nativeElement.querySelector('[data-testid="guest-message"]');
    expect(guestMessage).toBeTruthy();
    expect(guestMessage.textContent).toContain('Faça login para continuar');
  });
 
  it('deve exibir saudação personalizada quando autenticado', () => {
    isAuthenticatedSignal.set(true);
    component.userName = 'Maria';
    fixture.detectChanges();
 
    const heading = fixture.nativeElement.querySelector('h2');
    expect(heading.textContent).toContain('Bem-vindo, Maria!');
  });
 
  it('deve chamar logout ao clicar no botão de sair', () => {
    isAuthenticatedSignal.set(true);
    fixture.detectChanges();
 
    const logoutButton =
      fixture.nativeElement.querySelector('[data-testid="logout-btn"]');
    logoutButton.click();
    fixture.detectChanges();
 
    expect(mockAuthService.logout).toHaveBeenCalledOnce();
  });
});

Observe como usamos vi.fn() para criar mocks, que é a API nativa do Vitest. A sintaxe é praticamente idêntica ao jest.fn(), o que facilita a migração.

Boas práticas para Angular com Vitest

Organize os mocks de forma reutilizável

Em projetos reais, você terá dezenas de serviços. Crie uma pasta src/testing/mocks com fábricas de mock para os serviços mais usados:

TYPESCRIPT
// src/testing/mocks/auth.service.mock.ts
import { signal } from '@angular/core';
 
export function createMockAuthService() {
  return {
    isAuthenticated: signal(false),
    login: vi.fn(),
    logout: vi.fn(),
    getToken: vi.fn().mockReturnValue(null),
  };
}

Prefira data-testid para seletores

Evite selecionar elementos por classe CSS ou por tag HTML. Atributos como data-testid criam um contrato explícito entre o template e o teste, tornando os testes resilientes a mudanças visuais.

Use o modo watch no desenvolvimento

Execute vitest (sem o run) durante o desenvolvimento. O Vitest vai detectar mudanças nos arquivos e re-executar apenas os testes afetados. Em projetos com centenas de testes, isso economiza minutos a cada iteração.

Guias Relacionados

Explore mais guias técnicos

Mais de 100 guias práticos de integração entre frameworks, ferramentas e plataformas.

Ver todos os guias