Testes em JavaScript
Deep dive em estratégias, ferramentas e patterns de teste na stack JavaScript/TypeScript moderna (2026). Para fundamentos gerais de testes, ver Testes. Para Java testing, ver Testes em Java. Para React specifically, ver React.
O que é
Testar em JavaScript em 2026 é radicalmente diferente de 2020. Vitest está comendo o market share do Jest, Testing Library substituiu Enzyme, Playwright dominou E2E, e MSW resolveu o problema de mockar HTTP. Para um senior, o stack moderno é:
┌─────────────────────────────────────────┐
│ Stack de testes JS │
├─────────────────────────────────────────┤
│ Vitest / Jest — unit + integ │
│ Testing Library — component │
│ MSW (Mock Service Worker) — HTTP mocks │
│ Playwright / Cypress — E2E │
│ Storybook — component dev │
│ Chromatic / Percy — visual │
└─────────────────────────────────────────┘
Em entrevistas, o que diferencia um senior em testes JS:
- Saber escolher a ferramenta — Vitest vs Jest, Playwright vs Cypress
- Testing Library philosophy — “test what users see, not implementation”
- MSW — mockar HTTP no nível de rede, não do fetch
- Cobertura não é meta — entender métricas e suas armadilhas
- Test pyramid — muitas unit, menos integration, poucas E2E
- Fake timers, clock, fetch — controle total de side effects
- Strategy para components — user-event, fire-event, renderHook
- E2E patterns — page object model, fixtures, parallelization
- Flakiness — como detectar e evitar
Vitest vs Jest
Em 2026, Vitest é o default para novos projetos. Jest ainda domina projetos legacy (especialmente CRA/Next.js), mas Vitest está ganhando rápido.
Comparação
| Aspecto | Jest | Vitest |
|---|---|---|
| Ano | 2014 (Facebook) | 2021 (Vitest team) |
| Performance | Médio | 3-10x mais rápido |
| ESM | Suporte parcial, doloroso | Nativo |
| TypeScript | ts-jest (lento) ou SWC | Nativo via Vite |
| API | describe, it, expect | Compatível com Jest |
| Watch mode | Bom | Excelente (HMR) |
| UI dashboard | ❌ | ✅ built-in |
| Configuração | Verbosa | Herda do Vite |
| Mocking | jest.mock() | vi.mock() (compatível) |
| Snapshots | ✅ | ✅ |
| Coverage | ✅ (v8 ou babel) | ✅ (v8 native) |
Regra prática em 2026:
- Projeto novo → Vitest
- Projeto Vite/Next.js 14+ → Vitest
- Projeto Jest existente funcionando bem → fique no Jest até ter razão para migrar
Vitest — setup básico
npm install -D vitest @vitest/ui// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true, // usa describe/it/expect sem import
environment: 'jsdom', // ou 'node', 'happy-dom'
setupFiles: ['./tests/setup.ts'],
coverage: {
provider: 'v8',
reporter: ['text', 'html', 'lcov'],
exclude: ['**/node_modules/**', '**/dist/**', '**/*.config.*']
}
}
});// package.json
{
"scripts": {
"test": "vitest",
"test:ui": "vitest --ui",
"test:coverage": "vitest run --coverage",
"test:ci": "vitest run"
}
}Vitest — API essencial
Anatomia de um teste
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
describe('PatientService', () => {
let service: PatientService;
beforeEach(() => {
service = new PatientService();
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('create', () => {
it('should create a patient with valid data', () => {
// Arrange
const input = { name: 'Maria', email: 'maria@test.com' };
// Act
const patient = service.create(input);
// Assert
expect(patient.id).toBeDefined();
expect(patient.name).toBe('Maria');
});
it('should throw when email is invalid', () => {
expect(() => service.create({ name: 'X', email: 'invalid' }))
.toThrow('Invalid email');
});
});
});AAA Pattern (Arrange-Act-Assert) é universal. Torna testes legíveis em segundos.
Nomes descritivos:
// RUIM
it('works', () => { ... });
// BOM
it('should return 404 when patient not found', () => { ... });
it('should send welcome email on successful registration', () => { ... });Assertions comuns
// Equality
expect(value).toBe(5); // Object.is
expect(obj).toEqual({ name: 'Maria' }); // deep equality
expect(obj).toStrictEqual(other); // deep + type check
// Truthiness
expect(value).toBeTruthy();
expect(value).toBeFalsy();
expect(value).toBeNull();
expect(value).toBeUndefined();
expect(value).toBeDefined();
expect(value).toBeNaN();
// Numbers
expect(n).toBeGreaterThan(5);
expect(n).toBeGreaterThanOrEqual(5);
expect(n).toBeLessThan(10);
expect(n).toBeCloseTo(0.3, 5); // float tolerance
// Strings
expect(str).toMatch(/regex/);
expect(str).toContain('substring');
expect(str).toHaveLength(10);
// Arrays
expect(arr).toContain(item);
expect(arr).toContainEqual({ id: 1 }); // deep
expect(arr).toHaveLength(3);
// Objects
expect(obj).toHaveProperty('nested.field');
expect(obj).toHaveProperty('name', 'Maria');
expect(obj).toMatchObject({ name: 'Maria' }); // partial
// Exceptions
expect(() => fail()).toThrow();
expect(() => fail()).toThrow('specific message');
expect(() => fail()).toThrow(CustomError);
await expect(asyncFail()).rejects.toThrow();
// Async
await expect(fetchUser(1)).resolves.toEqual(user);
await expect(fetchUser(999)).rejects.toThrow('not found');Parametrização — test.each
describe('classify age', () => {
it.each([
[17, 'minor'],
[18, 'adult'],
[65, 'senior'],
[0, 'baby']
])('classify(%i) should return %s', (age, expected) => {
expect(classify(age)).toBe(expected);
});
});Com objetos para clareza:
it.each([
{ input: 'hello', expected: 'HELLO' },
{ input: 'Foo', expected: 'FOO' },
{ input: '', expected: '' }
])('uppercase($input) → $expected', ({ input, expected }) => {
expect(uppercase(input)).toBe(expected);
});Hooks
beforeAll(() => { /* uma vez no início */ });
afterAll(() => { /* uma vez no fim */ });
beforeEach(() => { /* antes de cada teste */ });
afterEach(() => { /* depois de cada teste */ });Regra: beforeEach é mais previsível que beforeAll porque cada teste começa em estado limpo. Use beforeAll apenas para setups caros e compartilháveis.
Skip, only, todo
it.skip('pending feature', () => { }); // pula
it.only('focus', () => { }); // só este no file
it.todo('later'); // planejado
describe.skip('pending suite', () => { });Cuidado: it.only em commit bloqueia CI se não for detectado. Use ESLint no-only-tests.
Concurrent
describe.concurrent('group', () => {
it.concurrent('test 1', async () => { ... });
it.concurrent('test 2', async () => { ... });
it.concurrent('test 3', async () => { ... });
});
// Rodam em paralelo (cuidado com shared state)Mocks em Vitest (vi)
vi.fn() — criar mock
const fn = vi.fn();
fn('a');
fn('b', 2);
expect(fn).toHaveBeenCalled();
expect(fn).toHaveBeenCalledTimes(2);
expect(fn).toHaveBeenCalledWith('a');
expect(fn).toHaveBeenLastCalledWith('b', 2);
expect(fn).toHaveBeenNthCalledWith(1, 'a');
// Configurar retorno
fn.mockReturnValue(42);
fn.mockReturnValueOnce(1).mockReturnValueOnce(2);
fn.mockResolvedValue('async');
fn.mockRejectedValue(new Error('fail'));
fn.mockImplementation((x) => x * 2);vi.spyOn — espiar método existente
const spy = vi.spyOn(console, 'log').mockImplementation(() => {});
service.log('hello');
expect(spy).toHaveBeenCalledWith('hello');
spy.mockRestore(); // volta ao normalvi.mock — mockar módulo inteiro
// test file
import { sendEmail } from './email';
import { createUser } from './user-service';
vi.mock('./email', () => ({
sendEmail: vi.fn().mockResolvedValue({ sent: true })
}));
describe('createUser', () => {
it('should send welcome email', async () => {
await createUser({ name: 'Maria', email: 'maria@test.com' });
expect(sendEmail).toHaveBeenCalledWith(
'maria@test.com',
expect.stringContaining('welcome')
);
});
});Parcial mock — keep original + override
vi.mock('./utils', async (importOriginal) => {
const actual = await importOriginal<typeof import('./utils')>();
return {
...actual,
getCurrentTime: vi.fn().mockReturnValue(new Date('2026-04-11'))
};
});Auto mock
vi.mock('./api'); // todas as exports viram mocks vazios
// Use __mocks__/api.ts para comportamento defaultClear vs reset vs restore
vi.clearAllMocks(); // limpa call history, mantém implementações
vi.resetAllMocks(); // limpa history + implementações (volta a retornar undefined)
vi.restoreAllMocks(); // restaura implementações originais (usado com spyOn)Regra: afterEach(() => vi.restoreAllMocks()) é um default seguro.
Mock de timer
it('should wait before sending', async () => {
vi.useFakeTimers();
const cb = vi.fn();
setTimeout(cb, 1000);
expect(cb).not.toHaveBeenCalled();
vi.advanceTimersByTime(1000); // "avança" o tempo
expect(cb).toHaveBeenCalled();
vi.useRealTimers();
});Operações comuns:
vi.advanceTimersByTime(ms); // avança N ms
vi.runAllTimers(); // executa todos os timers pendentes
vi.runOnlyPendingTimers(); // só os pendentes atuais
vi.setSystemTime(new Date('2026-04-11')); // fixa data/horaMock de fetch
// Manual — apenas para casos simples
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ id: 1, name: 'Maria' })
});
// PREFERIDO — MSW (abaixo)MSW (Mock Service Worker)
A maneira certa de mockar HTTP em JavaScript moderno. MSW intercepta requests no nível da rede (Service Worker no browser, interceptador no Node), sem mockar fetch ou axios.
Por que MSW
Antes:
// Ruim — mockando a biblioteca
vi.mock('axios', () => ({
default: { get: vi.fn().mockResolvedValue({ data: {...} }) }
}));Problemas:
- Se trocar de
axiosparafetch, os testes quebram - Mock é diferente do comportamento real (headers, serialization)
- Impossível simular erros de rede reais
Com MSW:
import { http, HttpResponse } from 'msw';
import { setupServer } from 'msw/node';
const server = setupServer(
http.get('/api/users/:id', ({ params }) => {
return HttpResponse.json({ id: params.id, name: 'Maria' });
}),
http.post('/api/users', async ({ request }) => {
const body = await request.json();
return HttpResponse.json({ id: 42, ...body }, { status: 201 });
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
// Seu código chama fetch normalmente, MSW intercepta
it('should fetch user', async () => {
const res = await fetch('/api/users/1');
const user = await res.json();
expect(user.name).toBe('Maria');
});Handlers dinâmicos
it('should handle 500 error', async () => {
server.use(
http.get('/api/users/:id', () => {
return new HttpResponse(null, { status: 500 });
})
);
const res = await fetch('/api/users/1');
expect(res.status).toBe(500);
});MSW também funciona em dev
// mocks/browser.ts
import { setupWorker } from 'msw/browser';
import { handlers } from './handlers';
export const worker = setupWorker(...handlers);
// src/main.tsx
if (process.env.NODE_ENV === 'development') {
const { worker } = await import('./mocks/browser');
await worker.start();
}Permite desenvolver frontend sem backend rodando. Um dos maiores ganhos de produtividade dos últimos anos.
Testing Library — testes de componentes
A filosofia: “test what users see, not implementation details”.
Em vez de testar “state”, “props” ou métodos internos, Testing Library encoraja testar:
- Comportamento visível — o que aparece na tela
- Interação do usuário — clicks, typing, form submit
- Acessibilidade — encontra elementos por role, label, text
Instalação
npm install -D @testing-library/react @testing-library/jest-dom @testing-library/user-event// tests/setup.ts
import '@testing-library/jest-dom/vitest'; // adiciona toBeInTheDocument, etc.Teste básico de componente React
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, it, expect } from 'vitest';
import { LoginForm } from './LoginForm';
describe('LoginForm', () => {
it('should render email and password fields', () => {
render(<LoginForm onSubmit={() => {}} />);
expect(screen.getByLabelText(/email/i)).toBeInTheDocument();
expect(screen.getByLabelText(/password/i)).toBeInTheDocument();
expect(screen.getByRole('button', { name: /log in/i })).toBeInTheDocument();
});
it('should call onSubmit with form data', async () => {
const user = userEvent.setup();
const onSubmit = vi.fn();
render(<LoginForm onSubmit={onSubmit} />);
await user.type(screen.getByLabelText(/email/i), 'maria@test.com');
await user.type(screen.getByLabelText(/password/i), 'secret');
await user.click(screen.getByRole('button', { name: /log in/i }));
expect(onSubmit).toHaveBeenCalledWith({
email: 'maria@test.com',
password: 'secret'
});
});
it('should show error when password is too short', async () => {
const user = userEvent.setup();
render(<LoginForm onSubmit={() => {}} />);
await user.type(screen.getByLabelText(/password/i), '12');
await user.tab(); // blur
expect(await screen.findByText(/at least 6 characters/i)).toBeInTheDocument();
});
});Queries — como encontrar elementos
Prioridade (Testing Library recomenda nesta ordem):
-
getByRole— acessível por leitores de tela. Sua primeira escolha.screen.getByRole('button', { name: /submit/i }) screen.getByRole('heading', { level: 1 }) screen.getByRole('textbox', { name: /email/i }) -
getByLabelText— form fieldsscreen.getByLabelText(/password/i) -
getByPlaceholderText— quando não há label (evite, prefira label) -
getByText— texto visívelscreen.getByText(/welcome/i) screen.getByText('Exact text') -
getByDisplayValue— valor atual de form field -
getByAltText— imagensscreen.getByAltText(/logo/i) -
getByTitle—titleattribute -
getByTestId—data-testid. Último recurso.
Variantes
| Método | Retorna | Throws se não achar |
|---|---|---|
getBy... | Element | Sim |
queryBy... | Element ou null | Não |
findBy... | Promise | Sim (após timeout) |
getAllBy... | Element[] | Sim (se vazio) |
queryAllBy... | Element[] | Não |
findAllBy... | Promise<Element[]> | Sim |
Quando usar cada:
getBy— elemento que deve existir agora (teste falha se não existir)queryBy— para verificar que não existe (expect(queryByText(...)).toBeNull())findBy— elemento que aparece assincronamente (espera até encontrar ou timeout)
user-event vs fireEvent
userEvent simula interações reais: focus, typing, tab order, modifiers. Preferido.
import userEvent from '@testing-library/user-event';
const user = userEvent.setup();
await user.click(button);
await user.type(input, 'hello');
await user.keyboard('{Enter}');
await user.tab();
await user.hover(element);
await user.selectOptions(select, 'option1');
await user.upload(fileInput, new File(['content'], 'file.txt'));fireEvent dispara eventos DOM sintéticos diretamente. Mais baixo nível.
import { fireEvent } from '@testing-library/react';
fireEvent.click(button);
fireEvent.change(input, { target: { value: 'hello' } });Regra: use userEvent sempre. fireEvent só em casos específicos (ex.: disparar eventos customizados).
Assertions com jest-dom
expect(element).toBeInTheDocument();
expect(element).not.toBeInTheDocument();
expect(element).toBeVisible();
expect(element).toBeDisabled();
expect(element).toBeEnabled();
expect(element).toBeChecked();
expect(element).toHaveFocus();
expect(element).toHaveTextContent('text');
expect(element).toHaveValue('value');
expect(element).toHaveAttribute('href', '/home');
expect(element).toHaveClass('active');
expect(element).toHaveStyle({ color: 'red' });
expect(form).toHaveFormValues({ email: 'test@test.com', remember: true });Hooks — renderHook
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';
it('should increment', () => {
const { result } = renderHook(() => useCounter());
expect(result.current.count).toBe(0);
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});Anti-patterns em Testing Library
Não teste implementation details
// RUIM — testa state interno
expect(wrapper.state('count')).toBe(1);
// RUIM — testa método interno
wrapper.instance().increment();
// BOM — testa comportamento visível
expect(screen.getByText('Count: 1')).toBeInTheDocument();
await user.click(screen.getByRole('button', { name: /increment/i }));Evite querySelector e getElementById
// RUIM
const btn = container.querySelector('.submit-btn');
// BOM — baseado em acessibilidade
const btn = screen.getByRole('button', { name: /submit/i });Evite snapshots grandes
// RUIM — snapshot enorme, ninguém revisa
expect(container).toMatchSnapshot();
// MELHOR — snapshots pequenos ou asserts específicos
expect(screen.getByTestId('user-card')).toMatchSnapshot();Não use act() manualmente
// RUIM — act() manual
act(() => {
fireEvent.click(btn);
});
// BOM — userEvent já envolve em act
await user.click(btn);E2E Testing — Playwright
Playwright (Microsoft) é o líder atual de E2E testing em 2026. Substituiu amplamente Cypress em projetos novos.
Por que Playwright
| Aspecto | Playwright | Cypress |
|---|---|---|
| Browsers | Chromium, Firefox, WebKit | Chromium (+ Firefox experimental) |
| Linguagens | JS, TS, Python, .NET, Java | JS, TS |
| Multi-tab / iframe | ✅ | ⚠️ limitado |
| Paralelização | ✅ nativa | ✅ paga ou custom |
| API | async/await | chainable |
| Auto-wait | ✅ excelente | ✅ |
| Trace viewer | ✅ excelente | ✅ |
| Component testing | ✅ experimental | ✅ |
| Performance | Muito rápido | Bom |
Setup Playwright
npm init playwright@latestTeste básico
import { test, expect } from '@playwright/test';
test('login flow', async ({ page }) => {
// Navegar
await page.goto('/login');
// Interagir
await page.getByLabel('Email').fill('maria@test.com');
await page.getByLabel('Password').fill('secret123');
await page.getByRole('button', { name: 'Log in' }).click();
// Assert
await expect(page).toHaveURL('/dashboard');
await expect(page.getByText(/welcome, Maria/i)).toBeVisible();
});
test('should show error on invalid credentials', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill('wrong@test.com');
await page.getByLabel('Password').fill('wrong');
await page.getByRole('button', { name: 'Log in' }).click();
await expect(page.getByRole('alert')).toContainText(/invalid credentials/i);
});Locators
Playwright usa locators que se parecem com Testing Library queries:
page.getByRole('button', { name: /submit/i })
page.getByLabel('Email')
page.getByText('Welcome')
page.getByPlaceholder('Search...')
page.getByAltText('Logo')
page.getByTitle('Close')
page.getByTestId('user-card')
// CSS (último recurso)
page.locator('.my-class')
page.locator('#my-id')
// Filtering
page.getByRole('listitem').filter({ hasText: 'Maria' })
page.getByRole('listitem').first()
page.getByRole('listitem').nth(2)Auto-wait
Playwright espera automaticamente por condições antes de agir. Não precisa wait manual na maioria dos casos.
// Espera botão estar visible, enabled e no viewport antes de clicar
await page.getByRole('button').click();
// Se precisar esperar explicitamente
await page.waitForURL('/dashboard');
await page.waitForLoadState('networkidle');
await expect(page.getByText('Loaded')).toBeVisible(); // retry até visiblePage Object Model
Organize testes com POM para evitar duplicação:
// pages/LoginPage.ts
import { Page, Locator } from '@playwright/test';
export class LoginPage {
readonly page: Page;
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly submitButton: Locator;
readonly errorMessage: Locator;
constructor(page: Page) {
this.page = page;
this.emailInput = page.getByLabel('Email');
this.passwordInput = page.getByLabel('Password');
this.submitButton = page.getByRole('button', { name: 'Log in' });
this.errorMessage = page.getByRole('alert');
}
async goto() {
await this.page.goto('/login');
}
async login(email: string, password: string) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
}
// tests/login.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
test('login', async ({ page }) => {
const login = new LoginPage(page);
await login.goto();
await login.login('maria@test.com', 'secret');
await expect(page).toHaveURL('/dashboard');
});Fixtures customizadas
// tests/fixtures.ts
import { test as base } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
type MyFixtures = {
loginPage: LoginPage;
authenticatedPage: Page;
};
export const test = base.extend<MyFixtures>({
loginPage: async ({ page }, use) => {
await use(new LoginPage(page));
},
authenticatedPage: async ({ page }, use) => {
// Setup: login
await page.goto('/login');
await page.getByLabel('Email').fill('test@test.com');
await page.getByLabel('Password').fill('test');
await page.getByRole('button', { name: 'Log in' }).click();
await page.waitForURL('/dashboard');
await use(page);
// Teardown (opcional)
}
});
export { expect } from '@playwright/test';// Uso
import { test, expect } from './fixtures';
test('should see profile', async ({ authenticatedPage: page }) => {
await page.goto('/profile');
await expect(page.getByRole('heading', { name: /profile/i })).toBeVisible();
});Parallelization
// playwright.config.ts
export default defineConfig({
workers: process.env.CI ? 4 : undefined, // paralelização
fullyParallel: true,
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
{ name: 'mobile', use: { ...devices['iPhone 14'] } }
]
});Trace viewer
Playwright gera traces com screenshots, snapshots DOM e network, navegáveis passo a passo:
npx playwright test --trace on
npx playwright show-trace trace.zipDebugging E2E nunca foi tão fácil. Você vê exatamente o que o teste fez, cada frame, cada network call.
Component testing (experimental)
Playwright pode testar componentes React/Vue/Svelte em isolamento:
import { test, expect } from '@playwright/experimental-ct-react';
import { Button } from './Button';
test('Button click', async ({ mount }) => {
let clicked = false;
const component = await mount(
<Button onClick={() => { clicked = true; }}>Click me</Button>
);
await component.click();
expect(clicked).toBe(true);
});Cypress — alternativa
Ainda popular, especialmente em projetos existentes. API chainable:
describe('login', () => {
it('should login with valid credentials', () => {
cy.visit('/login');
cy.get('input[name="email"]').type('maria@test.com');
cy.get('input[name="password"]').type('secret{enter}');
cy.url().should('include', '/dashboard');
cy.contains(/welcome/i).should('be.visible');
});
});Em 2026, Playwright é a escolha mais comum em projetos novos. Cypress ainda tem ecossistema grande mas tem perdido mindshare.
Storybook — desenvolvimento de componentes
Storybook não é tecnicamente “testes”, mas é parte essencial do workflow de UI moderno.
// Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';
const meta: Meta<typeof Button> = {
title: 'UI/Button',
component: Button,
args: {
onClick: () => {}
}
};
export default meta;
type Story = StoryObj<typeof Button>;
export const Primary: Story = {
args: {
variant: 'primary',
children: 'Click me'
}
};
export const Disabled: Story = {
args: {
variant: 'primary',
disabled: true,
children: 'Disabled'
}
};Storybook + interaction tests:
export const LoggedInSuccessfully: Story = {
args: { onSubmit: () => {} },
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await userEvent.type(canvas.getByLabelText('Email'), 'maria@test.com');
await userEvent.type(canvas.getByLabelText('Password'), 'secret');
await userEvent.click(canvas.getByRole('button', { name: /log in/i }));
await expect(canvas.getByText(/welcome/i)).toBeInTheDocument();
}
};Stories viram testes automaticamente via test-storybook.
Chromatic ou Percy fazem testes visuais — screenshots comparados entre commits, diffs visíveis.
Estratégia de testes para projeto moderno
Pirâmide adaptada para frontend
┌────────────┐
│ E2E │ Playwright
│ (poucos) │ 5-10 fluxos críticos
└────────────┘
┌────────────────┐
│ Integration │ Vitest + Testing Library + MSW
│ (muitos) │ componentes completos com rede mockada
└────────────────┘
┌─────────────────────┐
│ Component │ Vitest + Testing Library
│ (muitos) │ componentes isolados
└─────────────────────┘
┌──────────────────────────┐
│ Unit (muitos) │ Vitest
│ functions puras, hooks │
└──────────────────────────┘
Distribuição recomendada
- 60-70% unit tests — funções puras, hooks, utilities, reducers
- 20-30% component tests — Testing Library + MSW
- 5-10% E2E — fluxos críticos de negócio
Tempo de execução
- Unit: < 1s por teste, suite total < 30s
- Component: 100-500ms, suite < 2min
- E2E: 5-30s por teste, suite < 10min
- CI total: < 15min idealmente
O que testar em cada nível
Unit:
- Utilities puras (formatação, validação, cálculo)
- Hooks customizados (useCounter, useDebounce)
- Redux/Zustand reducers e actions
- Funções de negócio
Component:
- Componente renderiza com props
- Interação do usuário muda estado visível
- Loading, error, empty states
- Formulários (validação, submit)
Integration (dentro do Vitest):
- Múltiplos componentes trabalhando juntos
- Com router, context, estado global
- Com rede mockada via MSW
E2E:
- Login/logout
- Fluxo crítico de conversão (checkout, registration)
- Integrações complexas (upload de arquivo, real-time)
- Smoke tests de deploy
Cobertura de código — o que importa
vitest run --coverageMétricas
- Lines — quantas linhas foram executadas
- Statements — declarações executadas
- Functions — funções chamadas
- Branches — ramos de
if/else, ternários
Metas típicas
- 80% overall — razoável, não é meta de qualidade
- 90%+ para lógica de negócio crítica
- 100% é frequentemente waste — código trivial ou ramos impossíveis
Armadilhas
- 100% coverage não garante qualidade. Testes sem asserts contam para coverage.
- Coverage de tipos não existe em JS puro — use TypeScript.
- Coverage como meta → devs escrevem testes ruins para bater número.
- Branch coverage é mais importante que line coverage — pega ramos não testados.
- Use mutation testing (Stryker) para avaliar qualidade dos testes.
Stryker — mutation testing
npm install -D @stryker-mutator/core @stryker-mutator/vitest-runner
npx stryker init
npx stryker runAltera bytecode/código (mutações) e verifica se testes pegam. Métrica real de qualidade, não só coverage.
Flakiness — detectar e evitar
Flaky test = teste que às vezes passa, às vezes falha, sem mudança no código. Dívida técnica cara.
Causas comuns
- Timing — race conditions,
setTimeoutvswaitFor - Shared state — testes interferindo entre si
- Ordem — teste só passa se rodado numa ordem específica
- Depêndencias externas — rede, filesystem, relógio real
- Async mal tratado —
awaitesquecido - CSS transitions — E2E clica em elemento que ainda está animando
Prevenção
- Isolamento total — nada compartilhado entre testes
- Fake timers em vez de sleep real
- MSW em vez de rede real
findBy*em vez degetBy*+ setTimeout- Não confie em ordem —
beforeEach, nãobeforeAllcom state - Retry em E2E — Playwright tem retry built-in
Retry strategy
// Vitest
// vitest.config.ts
export default defineConfig({
test: {
retry: 1 // retry 1x em falha (máscara flakiness, use com cuidado)
}
});
// Playwright
// playwright.config.ts
export default defineConfig({
retries: process.env.CI ? 2 : 0
});Atenção: retries mascaram flakiness. Use como safety net, não como solução.
Detectar flakes
- Run all tests 10x em CI periódico
- Stryker — testes flaky falham nos “mutation runs”
- Playwright trace — inspeciona quando falha
Armadilhas comuns
it.onlycommitado — bloqueia CI. Use ESLintno-only-tests.- Coverage como meta em vez de ferramenta — devs gaming o número
- Mockar demais — se metade do teste é mock, o design tem problemas
- Testar implementação — wrapper.state(), wrapper.instance()
- querySelector em Testing Library — prefira role/label/text
getByTextcom texto dinâmico — regex mais seguro- async/await esquecido — teste passa silenciosamente
expect(1).toBe(1)— teste placeholder esquecido- Testes dependentes — teste 2 só passa se teste 1 rodou
- Mock global não limpo —
vi.restoreAllMocks()em afterEach - Real timers em testes de debounce — use fake timers
- Sleep real em E2E — use waitFor/auto-wait
- H2 em vez de Testcontainers (backend) — ver Testes em Java
- Testar bibliotecas de terceiros — confie que React funciona
- Não testar error paths — só happy path
- Snapshots enormes — ninguém revisa
- E2E para tudo — pirâmide invertida, lento e frágil
- Ignorar flakes — “roda de novo que passa” é dívida
- Shared context no beforeAll — vaza state entre testes
toHaveBeenCalled()semtoHaveBeenCalledWith— não valida argumentos
Na prática (da minha experiência)
Stack de testes no MedEspecialista:
1. Vitest + React Testing Library + MSW: Stack default para frontend. Vitest roda em ~3s para ~800 testes, Jest demorava 20s. Watch mode instantâneo graças ao HMR do Vite. MSW mocka toda a API, testes não dependem de backend rodando.
2. Playwright para E2E: Migrei do Cypress há um ano. Multi-browser (Chromium, Firefox, WebKit), paralelização nativa, trace viewer incrível. Debugging de falhas ficou 10x mais fácil.
3. MSW também em dev: Frontend consegue rodar standalone com MSW servindo dados fake. Um desenvolvedor do frontend não precisa ter o backend Spring Boot rodando. Onboarding de novos devs ficou trivial.
4. Fixtures compartilhadas via factories:
// tests/factories.ts export const makePatient = (overrides?: Partial<Patient>): Patient => ({ id: faker.number.int(), name: faker.person.fullName(), email: faker.internet.email(), birthDate: faker.date.past({ years: 60 }), ...overrides });Cada teste cria os objetos que precisa, sem depender de fixtures compartilhadas.
5.
data-testidsó em último caso: 95% dos queries usam role, label ou text.data-testidé fallback para casos onde não há semântica natural.6. Visual regression via Chromatic: Componentes em Storybook, snapshots visuais em cada PR. Pega regressões de CSS invisíveis em code review.
Incidente memorável — race condition em teste:
Teste que fazia fetch e depois assert era flaky. Às vezes passava, às vezes falhava. Causa: o componente fazia 2 fetches (dados + user info), teste usava
getByTextque é síncrono e falhava no primeiro render. Solução:findByText(async) ouwaitFor. Lição: queries assíncronas (findBy*) são default seguro quando há network.Outro — Playwright flaky em CI:
Teste passava local, falhava em CI com “element not clickable”. Causa: modal com transição CSS de 300ms. Playwright tentava clicar antes da animação terminar. Solução:
await expect(modal).toBeVisible()que espera estabilização, outoBeInViewport(). Playwright tem auto-wait, mas CSS transitions às vezes confundem.Mutation testing:
Rodei Stryker num módulo crítico de cálculo de preços. Coverage dizia 92%, Stryker mostrou 67% de mutações sobreviveram. Testes validavam “rodou” mas não “resultado correto”. Melhorei asserções baseado no relatório. Coverage é uma âncora, não um destino.
A lição principal: testes são investimento com retorno composto. Uma suite rápida e confiável é o que permite refactoring agressivo sem medo. Invista em velocidade (Vitest), em isolamento (MSW, fixtures), e em cultura (“nunca commite
it.only”, “flake é bug”).
How to explain in English
“My JavaScript testing stack in 2026 is Vitest for unit and integration tests, React Testing Library for component tests, MSW for HTTP mocking, and Playwright for end-to-end. I migrated from Jest to Vitest over a year ago and the difference is dramatic — same API, but 3-10x faster, native ESM, and a proper UI dashboard.
For component tests, I follow Testing Library’s philosophy: test what users see, not implementation details. I use
getByRoleas my primary query because it aligns with accessibility, and I useuserEventto simulate real interactions like clicking, typing, and tabbing. I almost never usedata-testid— that’s a last resort.MSW is a game-changer. Instead of mocking
fetchoraxios, MSW intercepts at the network level. My tests don’t care how the HTTP client works, and the same handlers can be used in development mode so the frontend can run without a backend. That alone is worth the setup cost.For E2E, I use Playwright. The auto-wait is reliable, the trace viewer makes debugging CI failures trivial, and parallelization across browsers is native. I organize tests with the Page Object Model and custom fixtures for authentication and common setups. I keep E2E tests minimal — only critical business flows, because they’re slow and more prone to flakiness.
My test distribution is roughly 70% unit, 25% component, 5% E2E. The pyramid, not the ice cream cone. I aim for sub-30-second unit suite and sub-10-minute CI total.
I don’t chase 100% coverage. I chase test quality. I run Stryker mutation testing occasionally on critical code — it reveals tests that execute code but don’t actually verify behavior. Coverage is a floor, not a ceiling.
Flakiness is dead debt. If a test is flaky, I fix it or delete it. Auto-retry in CI masks the problem. My rules: isolated tests, fake timers for anything timing-related, MSW for network, and
findBy*for async-appearing elements.”
Frases úteis em entrevista
- “Vitest in 2026 — Jest is still valid for legacy, Vitest for new projects.”
- “Testing Library philosophy: test what users see, not implementation.”
- “MSW at the network level — not mocking fetch directly.”
- “userEvent over fireEvent — simulates real user interaction.”
- “
getByRolefirst,getByLabelTextsecond,data-testidlast resort.” - “Playwright for E2E with trace viewer for debugging CI failures.”
- “Page Object Model and custom fixtures keep E2E maintainable.”
- “Fake timers for debounce, throttle, and any time-dependent logic.”
- “Test pyramid — not the ice cream cone.”
- “Mutation testing reveals test quality better than coverage.”
- “Flaky tests are bugs. Auto-retry hides, doesn’t fix.”
Key vocabulary
- teste unitário → unit test
- teste de componente → component test
- teste de integração → integration test
- teste de ponta a ponta → end-to-end test (E2E)
- regressão visual → visual regression
- simulação / mock → mock / stub / fake
- asserção → assertion
- correspondência → matcher
- consulta → query
- interceptor de rede → network interceptor
- temporizador falso → fake timer
- instantâneo → snapshot
- teste instável → flaky test
- cobertura → coverage
- teste de mutação → mutation testing
- pirâmide de testes → test pyramid
- interação do usuário → user interaction
- visualizador de rastros → trace viewer
Recursos
Documentação
Livros
- Testing JavaScript — Kent C. Dodds (curso + livro)
- Test-Driven Development with Node.js — Prince Abalogu
- Unit Testing Principles, Practices, and Patterns — Vladimir Khorikov (conceitos universais)
Artigos
- Kent C. Dodds Blog — filosofia de Testing Library (ele escreveu)
- Testing Trophy — Kent C. Dodds
- Common mistakes with React Testing Library
- Why MSW
Vídeos
- Kent C. Dodds YouTube — Testing Library, Testing Trophy
- Playwright official YouTube — demos e tutoriais
- “Testing Like the TSA” by Kent C. Dodds
Ferramentas
- Stryker — mutation testing
- Chromatic — visual regression
- Percy — visual regression
- BackstopJS — visual regression
- happy-dom — alternativa ao jsdom, mais rápido
- faker-js — geração de dados de teste
Veja também
- JavaScript Fundamentals — linguagem base
- TypeScript — tipagem em testes
- React — React-specific testing
- Node.js — backend testing
- Testes — fundamentos gerais
- Testes em Java — comparação com stack Java
- Full Stack Open - Guia de Revisão — partes 4, 5, 10 cobrem testes
- API Design — contract testing