Capítulo 16: Testes Automatizados com Vitest e Vue Test Utils
Capítulo 16: Testes Automatizados com Vitest e Vue Test Utils
Especialização em Front-End com Vue • Vue 3 & Nuxt 3 • Composition API, Pinia, Vue Router, SSR e TypeScript
🗺️ Mapa Conceitual da Pirâmide de Testes no Vue 3
flowchart TD
A["Código Fonte Vue 3 + TypeScript"] --> B["Test Runner: Vitest (Nativo do Vite)"]
subgraph NIVEIS_TESTE["Níveis de Validação Automatizada"]
C["1. Testes Unitários de Composables (Lógica Pura)"]
D["2. Testes de Componentes (@vue/test-utils + Happy-DOM)"]
E["3. Testes de Integração de Stores (Pinia Testing)"]
F["4. Testes End-to-End (Playwright / Cypress)"]
end
B --> NIVEIS_TESTE
D --> G["mount() / shallowMount()"]
G --> H["trigger('click') / setValue()"]
H --> I["await flushPromises() (Microtasks)"]
I --> J["expect(wrapper.emitted()).toHaveProperty(...)"]
style A fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
style B fill:#fff3e0,stroke:#ff9800,stroke-width:2px
style NIVEIS_TESTE fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
style J fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
🏛️ 1. Testes Automatizados de Alta Performance com Vitest
O Vitest é o executor de testes unitários e de integração oficial e recomendado para o ecossistema Vue 3. Por compartilhar exatamente a mesma pipeline de configuração (vite.config.ts), plugins e resolução de caminhos do Vite, o Vitest elimina a necessidade de duplicar configurações complexas de Babel ou ts-jest, executando testes com suporte nativo a ESM e velocidade 10x superior ao Jest tradicional.
1.1 O Papel da Biblioteca @vue/test-utils
A @vue/test-utils provê utilitários oficiais para montar e interagir com componentes Vue em um ambiente simulado de DOM (geralmente happy-dom ou jsdom):
mount(Component, options): Monta o componente completo, incluindo todos os seus componentes filhos e subárvores. É a abordagem recomendada para testes de integração de componentes funcionais.shallowMount(Component, options): Monta apenas o componente alvo, substituindo todos os componentes filhos por stubs vazios (<stub-component>). Útil para testes unitários isolados onde filhos realizam chamadas pesadas.
1.2 Simulação de Eventos do Usuário com trigger e setValue
Interações de usuário são assíncronas por natureza no Vue devido à reconciliação em lote do Virtual DOM:
const input = wrapper.find('input[type="text"]');
await input.setValue('Novo Valor'); // setValue atualiza o v-model e despacha eventos
const button = wrapper.find('button');
await button.trigger('click'); // Sempre use await para aguardar a microtarefa do DOM
1.3 flushPromises() e Resolução de Tarefas Assíncronas
Quando um componente dispara Promises (como chamadas a APIs ou timers assíncronos), usar apenas nextTick() pode não ser suficiente, pois nextTick() resolve apenas uma única passagem da fila de microtarefas.
- O utilitário
flushPromises()força a resolução de todas as microtarefas pendentes na fila do JavaScript antes que as asserções (expect) sejam avaliadas.
1.4 Testes de Stores com @pinia/testing
Para testar componentes que dependem de stores Pinia sem instanciar serviços reais de backend, utiliza-se createTestingPinia():
const wrapper = mount(MeuComponente, {
global: {
plugins: [createTestingPinia({
initialState: { auth: { user: { name: 'Admin' } } },
stubActions: false // Permite que actions reais executem se desejado
})]
}
});
⚖️ 2. Tabela Comparativa: Jest vs Vitest
| Critério de Avaliação | Jest (Legado) | Vitest (Moderno) |
|---|---|---|
| Pipeline de Compilação | Separada; requer Babel ou ts-jest duplicado. |
Idêntica ao Vite (mesmos aliases e plugins). |
| Suporte a ESM Nativo | Complexo e experimental. | Nativo de Primeira Classe. |
| Velocidade de Execução | Moderada a Lenta (sobrecarga de transformações). | Ultra-Rápida com cache de Vite e multithreading. |
| Watch Mode (HMR de Testes) | Lento em projetos grandes. | Instantâneo (re-executa apenas o teste alterado). |
| Compatibilidade de API | API canônica (describe, test, expect). |
100% compatível com a sintaxe do Jest. |
⚠️ 3. Armadilhas Comuns em Produção (Gotchas & Anti-Patterns)
Anti-Pattern 1: Esquecer o await ao Chamar trigger() ou setValue()
Erro Comum: Escrever wrapper.find('button').trigger('click'); e imediatamente na linha seguinte fazer expect(wrapper.text()).toContain(...).
Consequência: O teste falhará aleatoriamente (flaky test), pois o Vue ainda não concluiu a atualização física do DOM naquele ciclo de clock.
Anti-Pattern 2: Testar Detalhes Internos de Implementação
Erro Comum: Inspecionar o estado interno de variáveis privadas via wrapper.vm.minhaVariavelPrivada.
Abordagem Correta: Teste como o usuário enxerga e interage: forneça entradas (props, cliques, digitação) e valide saídas (texto renderizado na tela e eventos emitidos).
💻 4. Código Conceitual Arquitetural Comentado
// tests/unit/CounterCard.spec.ts
import { describe, it, expect } from 'vitest';
import { mount } from '@vue/test-utils';
import CounterCard from '@/components/CounterCard.vue';
describe('Componente CounterCard.vue', () => {
it('deve renderizar o título e o valor inicial passado por props', () => {
const wrapper = mount(CounterCard, {
props: {
title: 'Métrica de Conexões',
initialCount: 5
}
});
expect(wrapper.find('h3').text()).toBe('Métrica de Conexões');
expect(wrapper.find('[data-testid="counter-value"]').text()).toBe('5');
});
it('deve emitir o evento "incremented" com o novo valor ao clicar no botão', async () => {
const wrapper = mount(CounterCard, {
props: { title: 'Métrica', initialCount: 10 }
});
const btn = wrapper.find('[data-testid="btn-add"]');
await btn.trigger('click');
// Valida emissão do contrato de evento
expect(wrapper.emitted('incremented')).toBeTruthy();
expect(wrapper.emitted('incremented')?.[0]).toEqual([11]);
});
});
🛠️ 5. Cheat-Sheet de Comandos e Utilitários de Teste
# Execução da suíte no terminal
npx vitest run # Executa uma única vez (CI/CD)
npx vitest # Modo interativo com watch instantâneo
npx vitest --coverage # Gera relatório de cobertura de código
// Asserções frequentes com @vue/test-utils
expect(wrapper.find('.modal').exists()).toBe(false);
expect(wrapper.classes()).toContain('active');
expect(wrapper.attributes('disabled')).toBeDefined();
expect(wrapper.emitted('submit')).toHaveLength(1);