Test-Driven Development with Cursor: Unit, Integration, and E2E

Testing is essential for maintaining code quality and confidence in deployments. Cursor's AI capabilities make test-driven development (TDD) more accessible by generating tests, suggesting edge cases, and helping you achieve better coverage. This guide covers unit, integration, and end-to-end testing workflows.
Prerequisites
Before starting:
- Cursor installed (latest version)
- Node.js installed
- Basic understanding of testing concepts
Unit Testing with Jest
Setting Up Jest
Install Jest and related packages:
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,
},
},
};
Generating Unit Tests with Cursor
Select a function and ask Cursor to generate tests:
// 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:
Write comprehensive Jest unit tests for this function, including:
- Happy path tests
- Edge cases (zero, negative, boundary values)
- Error cases
- Tests for the maxDiscount parameter
Cursor generates:
// 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');
});
});
});
Testing React Components
// 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 for Modern Projects
Vitest Configuration
// 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,
},
},
},
});
Vitest Test Example
// 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();
});
});
Vitest offers native TypeScript support, faster execution, and better ESM compatibility. Use it for new projects; stick with Jest if you have an existing test suite.
Integration Testing
API Integration Tests
// 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();
});
});
});
Database Integration Setup
// 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();
}
End-to-End Testing
Cypress Setup
npm install --save-dev cypress
npx cypress open
Writing E2E Tests with 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');
});
});
Playwright Alternative
// 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();
});
Test Coverage and Quality
Coverage Reports
Generate and view coverage:
# Jest
npm test -- --coverage
# Vitest
npx vitest --coverage
AI-Assisted Coverage Improvement
Share your coverage report with Cursor:
My test coverage is at 65%. Here are the uncovered lines:
[paste coverage report]
Suggest tests to cover these edge cases.
Testing Best Practices
| Practice | Description |
|---|---|
| AAA Pattern | Arrange, Act, Assert - structure every test clearly |
| One assertion per test | Keep tests focused and readable |
| Descriptive names | Test names should read like documentation |
| Mock external deps | Isolate units by mocking databases, APIs |
| Test behavior, not implementation | Refactor-safe tests verify outcomes |
| Use data-testid | Add stable selectors for E2E tests |
Test Utilities and Helpers
// 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,
});
Running Tests in Cursor
Terminal Integration
# 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
Test Explorer
Cursor supports the VS Code Test Explorer:
- Open the Testing sidebar
- See all tests organized by file
- Click to run individual tests
- View pass/fail status inline
Continuous Integration
GitHub Actions Example
# .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
Quick Reference
| Task | Cursor Prompt |
|---|---|
| Generate unit tests | "Write Jest tests for this function with edge cases" |
| Mock external API | "Create a mock for this API client using MSW" |
| Fix failing test | "This test is failing with [error]. Fix it." |
| Improve coverage | "Suggest tests to improve coverage for this file" |
| E2E test flow | "Write a Cypress test for the user checkout flow" |
Related Resources
Last updated: July 2025