TDD com Cursor: Unitário, Integração e E2E

Testes são essenciais para manter a qualidade do código e a confiança nos deployments. Os recursos de IA do Cursor ajudam a gerar testes, sugerir casos de borda e alcançar melhor cobertura, tornando o Desenvolvimento Orientado a Testes (TDD) mais acessível. Este guia aborda fluxos de trabalho de testes unitários, de integração e end-to-end.
Pré-requisitos
Antes de começar, você precisará de:
- Cursor instalado (versão mais recente)
- Node.js instalado
- Noções básicas de conceitos de testes
Testes Unitários com Jest
Configuração do Jest
Instale o Jest e pacotes relacionados:
npm install --save-dev jest @types/jest ts-jest
Configure jest.config.js:
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/src'],
testMatch: ['**/__tests__/**/*.test.ts'],
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.d.ts',
'!src/**/*.test.ts',
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
};
Gerando Testes Unitários com Cursor
Selecione uma função e peça ao Cursor para gerar testes:
// src/utils/calculator.ts
export const calculateDiscount = (
price: number,
discountPercent: number,
maxDiscount?: number
): number => {
if (price < 0 || discountPercent < 0 || discountPercent > 100) {
throw new Error('Invalid input');
}
const discount = (price * discountPercent) / 100;
const finalDiscount = maxDiscount ? Math.min(discount, maxDiscount) : discount;
return Number((price - finalDiscount).toFixed(2));
};
Prompt:
Escreva testes unitários Jest completos para esta função. Devem incluir:
- Testes de caminho feliz
- Casos de borda (0, negativos, valores limite)
- Casos de erro
- Testes para o parâmetro maxDiscount
Resultado gerado pelo Cursor:
// src/utils/__tests__/calculator.test.ts
import { calculateDiscount } from '../calculator';
describe('calculateDiscount', () => {
describe('happy path', () => {
it('should calculate 10% discount correctly', () => {
expect(calculateDiscount(100, 10)).toBe(90);
});
it('should calculate 50% discount correctly', () => {
expect(calculateDiscount(200, 50)).toBe(100);
});
});
describe('maxDiscount parameter', () => {
it('should apply maxDiscount when discount exceeds it', () => {
expect(calculateDiscount(1000, 50, 100)).toBe(900);
});
it('should ignore maxDiscount when discount is smaller', () => {
expect(calculateDiscount(100, 10, 50)).toBe(90);
});
});
describe('edge cases', () => {
it('should return original price with 0% discount', () => {
expect(calculateDiscount(100, 0)).toBe(100);
});
it('should return 0 with 100% discount', () => {
expect(calculateDiscount(100, 100)).toBe(0);
});
it('should handle decimal prices', () => {
expect(calculateDiscount(99.99, 10)).toBe(89.99);
});
});
describe('error cases', () => {
it('should throw error for negative price', () => {
expect(() => calculateDiscount(-10, 10)).toThrow('Invalid input');
});
it('should throw error for negative discount', () => {
expect(() => calculateDiscount(100, -5)).toThrow('Invalid input');
});
it('should throw error for discount over 100%', () => {
expect(() => calculateDiscount(100, 101)).toThrow('Invalid input');
});
});
});
Testes de Componentes React
// src/components/Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { Button } from './Button';
describe('Button', () => {
it('renders with correct text', () => {
render(<Button>Click me</Button>);
expect(screen.getByText('Click me')).toBeInTheDocument();
});
it('calls onClick when clicked', () => {
const handleClick = jest.fn();
render(<Button onClick={handleClick}>Click me</Button>);
fireEvent.click(screen.getByText('Click me'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
it('is disabled when loading', () => {
render(<Button isLoading>Loading</Button>);
expect(screen.getByRole('button')).toBeDisabled();
});
});
Vitest para Projetos Modernos
Configuração do Vitest
// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['./src/test/setup.ts'],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
thresholds: {
lines: 80,
functions: 80,
branches: 80,
statements: 80,
},
},
},
});
Exemplo de Teste Vitest
// src/utils/formatDate.test.ts
import { describe, it, expect } from 'vitest';
import { formatDate } from './formatDate';
describe('formatDate', () => {
it('formats ISO date to readable string', () => {
const result = formatDate('2024-01-15');
expect(result).toBe('January 15, 2024');
});
it('handles invalid dates gracefully', () => {
expect(() => formatDate('invalid')).toThrow();
});
});
O Vitest oferece suporte nativo a TypeScript, execução mais rápida e melhor compatibilidade com ESM. Use Vitest para novos projetos e mantenha o Jest se você tem uma suíte de testes existente.
Testes de Integração
Testes de Integração de API
// tests/integration/users.test.ts
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { createApp } from '../../src/app';
import { setupTestDB, teardownTestDB } from '../helpers/database';
const app = createApp();
describe('Users API Integration', () => {
beforeAll(async () => {
await setupTestDB();
});
afterAll(async () => {
await teardownTestDB();
});
describe('POST /api/users', () => {
it('creates a user and persists to database', async () => {
const response = await app.request('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: 'Integration Test',
password: 'password123',
}),
});
expect(response.status).toBe(201);
const body = await response.json();
expect(body.data.id).toBeDefined();
});
});
});
Configuração de Integração com Banco de Dados
// tests/helpers/database.ts
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
export async function setupTestDB() {
// Run migrations
// Seed test data
}
export async function teardownTestDB() {
// Clean up test data
await prisma.$executeRaw`TRUNCATE TABLE users CASCADE`;
await prisma.$disconnect();
}
Testes End-to-End
Configuração do Cypress
npm install --save-dev cypress
npx cypress open
Escrevendo Testes E2E com Cursor
// cypress/e2e/auth.cy.js
describe('Authentication Flow', () => {
beforeEach(() => {
cy.visit('/login');
});
it('allows user to log in with valid credentials', () => {
cy.get('[data-testid="password-input"]').type('password123');
cy.get('[data-testid="login-button"]').click();
cy.url().should('include', '/dashboard');
cy.get('[data-testid="welcome-message"]').should('contain', 'Welcome');
});
it('shows error for invalid credentials', () => {
cy.get('[data-testid="password-input"]').type('wrongpassword');
cy.get('[data-testid="login-button"]').click();
cy.get('[data-testid="error-message"]')
.should('be.visible')
.and('contain', 'Invalid credentials');
});
});
Alternativa com Playwright
// tests/e2e/checkout.spec.ts
import { test, expect } from '@playwright/test';
test('user can complete checkout', async ({ page }) => {
await page.goto('/products');
// Add item to cart
await page.click('[data-testid="add-to-cart-1"]');
await page.click('[data-testid="cart-icon"]');
// Proceed to checkout
await page.click('[data-testid="checkout-button"]');
// Fill shipping info
await page.fill('[name="address"]', '123 Test St');
await page.fill('[name="city"]', 'Test City');
await page.selectOption('[name="country"]', 'US');
// Complete order
await page.click('[data-testid="place-order"]');
await expect(page.locator('[data-testid="order-confirmation"]')).toBeVisible();
});
Cobertura e Qualidade de Testes
Relatórios de Cobertura
Gere e visualize cobertura:
# Jest
npm test -- --coverage
# Vitest
npx vitest --coverage
Melhoria de Cobertura com IA
Compartilhe seu relatório de cobertura com o Cursor:
Minha cobertura de testes está em 65%. As linhas não cobertas são:
[Colar relatório de cobertura]
Sugira testes para cobrir esses casos de borda.
Melhores Práticas de Testes
| Prática | Descrição |
|---|---|
| Padrão AAA | Arrange, Act, Assert - estruture todos os testes claramente |
| Uma asserção por teste | Mantenha testes focados e legíveis |
| Nomes descritivos | Nomes de testes devem ler como documentação |
| Mock de dependências externas | Isole unidades mockando banco de dados e APIs |
| Teste comportamento, não implementação | Testes seguros para refatoração verificam resultados |
| Use data-testid | Adicione seletores estáveis para testes E2E |
Utilitários e Helpers de Teste
// src/test/factories.ts
import { faker } from '@faker-js/faker';
export const createUserFixture = (overrides = {}) => ({
id: faker.string.uuid(),
email: faker.internet.email(),
name: faker.person.fullName(),
createdAt: new Date(),
...overrides,
});
export const createProductFixture = (overrides = {}) => ({
id: faker.string.uuid(),
name: faker.commerce.productName(),
price: Number(faker.commerce.price()),
...overrides,
});
Executando Testes no Cursor
Integração com Terminal
# Run all tests
npm test
# Run with watch mode
npm test -- --watch
# Run specific file
npm test -- src/utils/calculator.test.ts
# Run with coverage
npm test -- --coverage
Explorador de Testes
O Cursor suporta o explorador de testes do VS Code:
- Abra a barra lateral de testes
- Veja todos os testes organizados por arquivo
- Clique para executar testes individuais
- Veja status de pass/fail inline
Integração Contínua
Exemplo com GitHub Actions
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: npm test
- run: npm run test:e2e
Referência Rápida
| Tarefa | Prompt do Cursor |
|---|---|
| Gerar testes unitários | "Write Jest tests for this function with edge cases" |
| Mock de API externa | "Create a mock for this API client using MSW" |
| Corrigir teste quebrado | "This test is failing with [error]. Fix it." |
| Melhorar cobertura | "Suggest tests to improve coverage for this file" |
| Teste E2E de fluxo | "Write a Cypress test for the user checkout flow" |
Recursos Relacionados
- Guia de Desenvolvimento de API do Cursor
- Guia de Debug React do Cursor
- Guia de Agentes em Segundo Plano do Cursor
Última atualização: Julho de 2025