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:
- O tempo de inicialização é alto, mesmo para poucos testes.
- A configuração do
karma.conf.jsjunto com oangular.jsoné verbosa e propensa a erros. - 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,expectevi.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
// Execute no terminal:
// npm install -D vitest @analogjs/vite-angular jsdomO 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:
/// <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: truepermite usardescribe,iteexpectsem importação explícita.environment: 'jsdom'simula o DOM no Node.js.setupFilesaponta para um arquivo de inicialização que veremos a seguir.includedefine o padrão de busca para arquivos de teste.
Passo 3: criar o arquivo de setup
Crie o arquivo src/test-setup.ts:
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:
{
"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
// 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
// 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
// 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
// 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
// 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:
// 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