Test de code avec Cursor : Unités, Intégration et E2E

Les tests sont la pierre angulaire du développement logiciel fiable. Les capacités IA de Cursor peuvent vous aider à écrire des suites de tests complètes, identifier les cas limites et maintenir la couverture de test. Ce guide couvre les workflows de tests unitaires, d'intégration et de bout en bout.
Prérequis
Avant de commencer :
- Cursor installé (dernière version recommandée)
- Une base de code avec un framework de test configuré
- Familiarité avec les concepts de test (assertions, mocks, fixtures)
Configuration des tests dans Cursor
Structure du projet
project/
src/
components/
utils/
api/
tests/
unit/ # Tests unitaires
integration/ # Tests d'intégration
e2e/ # Tests de bout en bout
jest.config.js
playwright.config.ts
vitest.config.ts
Configuration de Jest avec TypeScript
// jest.config.js
module.exports = {
preset: 'ts-jest',
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/tests/setup.ts'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
'\\.(css|less|scss)$': 'identity-obj-proxy',
},
collectCoverageFrom: [
'src/**/*.{ts,tsx}',
'!src/**/*.d.ts',
'!src/**/index.ts',
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
};
Écriture de tests unitaires avec l'IA
Génération de tests à partir du code
Sélectionnez un snippet de code et utilisez la commande Ctrl+L de Cursor :
Écris des tests unitaires complets pour cette fonction :
- Cas nominaux
- Cas limites (entrée vide, très grande valeur, caractères spéciaux)
- Gestion des erreurs
- Conditions aux limites
Exemple : tester des fonctions utilitaires
// src/utils/validation.ts
export const isValidEmail = (email: string): boolean => {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(email);
};
export const formatDate = (
date: Date,
format: string = 'YYYY-MM-DD'
): string => {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return format
.replace('YYYY', String(year))
.replace('MM', month)
.replace('DD', day);
};
Cursor génère les tests :
// tests/unit/validation.test.ts
import { isValidEmail, formatDate } from '@/utils/validation';
describe('isValidEmail', () => {
it('returns true for valid email', () => {
});
it('returns false for invalid email', () => {
expect(isValidEmail('')).toBe(false);
expect(isValidEmail('not-an-email')).toBe(false);
expect(isValidEmail('@domain.com')).toBe(false);
expect(isValidEmail('user@')).toBe(false);
expect(isValidEmail('user@domain')).toBe(false);
});
it('handles edge cases', () => {
});
});
describe('formatDate', () => {
it('formats date with default format', () => {
const date = new Date('2024-03-15');
expect(formatDate(date)).toBe('2024-03-15');
});
it('formats date with custom format', () => {
const date = new Date('2024-03-15');
expect(formatDate(date, 'DD/MM/YYYY')).toBe('15/03/2024');
expect(formatDate(date, 'MM-DD-YYYY')).toBe('03-15-2024');
});
it('handles edge cases', () => {
const janFirst = new Date('2024-01-01');
expect(formatDate(janFirst)).toBe('2024-01-01');
const dec31 = new Date('2024-12-31');
expect(formatDate(dec31)).toBe('2024-12-31');
});
});
Tester des composants React
// tests/unit/Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { Button } from '@/components/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();
});
it('shows loading spinner when isLoading is true', () => {
render(<Button isLoading>Loading</Button>);
expect(screen.getByTestId('spinner')).toBeInTheDocument();
});
});
Tests d'intégration
Tester des endpoints API
// tests/integration/api.test.ts
import request from 'supertest';
import { app } from '@/app';
import { prisma } from '@/db';
describe('API Integration Tests', () => {
beforeEach(async () => {
await prisma.user.deleteMany();
});
afterAll(async () => {
await prisma.$disconnect();
});
describe('POST /api/users', () => {
it('creates a new user', async () => {
const response = await request(app)
.post('/api/users')
.send({
name: 'Test User',
password: 'password123',
})
.expect(201);
});
it('returns 400 for invalid data', async () => {
await request(app)
.post('/api/users')
.send({
email: 'invalid',
name: '',
})
.expect(400);
});
});
});
Tester les interactions avec la base de données
// tests/integration/database.test.ts
import { UserRepository } from '@/repositories/UserRepository';
import { prisma } from '@/db';
describe('UserRepository', () => {
let repository: UserRepository;
beforeEach(() => {
repository = new UserRepository();
});
afterEach(async () => {
await prisma.user.deleteMany();
});
it('creates and retrieves a user', async () => {
const user = await repository.create({
name: 'Test User',
});
expect(user.id).toBeDefined();
const found = await repository.findById(user.id);
});
});
Tests de bout en bout avec Playwright
Configuration de Playwright
npm init playwright@latest
Génération de tests E2E
// tests/e2e/login.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Authentication Flow', () => {
test('user can log in', async ({ page }) => {
await page.goto('/login');
await page.fill('[data-testid="password-input"]', 'password123');
await page.click('[data-testid="login-button"]');
await expect(page).toHaveURL('/dashboard');
await expect(page.locator('[data-testid="welcome-message"]')).toContainText(
'Welcome'
);
});
test('shows error for invalid credentials', async ({ page }) => {
await page.goto('/login');
await page.fill('[data-testid="password-input"]', 'wrongpassword');
await page.click('[data-testid="login-button"]');
await expect(page.locator('[data-testid="error-message"]')).toContainText(
'Invalid credentials'
);
});
});
Pattern Page Object avec Cursor
// tests/e2e/pages/LoginPage.ts
import { Page, Locator } from '@playwright/test';
export class LoginPage {
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly loginButton: Locator;
readonly errorMessage: Locator;
constructor(private page: Page) {
this.emailInput = page.locator('[data-testid="email-input"]');
this.passwordInput = page.locator('[data-testid="password-input"]');
this.loginButton = page.locator('[data-testid="login-button"]');
this.errorMessage = page.locator('[data-testid="error-message"]');
}
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.loginButton.click();
}
}
Développement piloté par les tests avec Cursor
Cycle rouge-vert-refactor
- Rouge : Écrivez un test décrivant le comportement attendu
- Vert : Utilisez Cursor pour générer le code d'implémentation
- Refactor : Demandez à Cursor de nettoyer le code tout en gardant les tests passants
Exemple : TDD avec Cursor
// Étape 1 : Écrivez le test
// tests/unit/calculator.test.ts
import { Calculator } from '@/utils/Calculator';
describe('Calculator', () => {
it('adds two numbers', () => {
const calc = new Calculator();
expect(calc.add(2, 3)).toBe(5);
});
it('handles negative numbers', () => {
const calc = new Calculator();
expect(calc.add(-2, 3)).toBe(1);
});
});
// Étape 2 : Cursor génère l'implémentation
// src/utils/Calculator.ts
export class Calculator {
add(a: number, b: number): number {
return a + b;
}
}
Mocks et stubs
Mocks Jest
// tests/unit/api.test.ts
import { fetchUserData } from '@/api/users';
import { getDatabaseConnection } from '@/db';
jest.mock('@/db', () => ({
getDatabaseConnection: jest.fn(),
}));
describe('fetchUserData', () => {
it('returns user data from database', async () => {
const mockQuery = jest.fn().mockResolvedValue({
rows: [{ id: 1, name: 'Test User' }],
});
(getDatabaseConnection as jest.Mock).mockReturnValue({
query: mockQuery,
});
const result = await fetchUserData(1);
expect(result).toEqual({ id: 1, name: 'Test User' });
expect(mockQuery).toHaveBeenCalledWith(
'SELECT * FROM users WHERE id = $1',
[1]
);
});
});
MSW pour le mocking d'API
// tests/mocks/handlers.ts
import { rest } from 'msw';
export const handlers = [
rest.get('/api/users', (req, res, ctx) => {
return res(
ctx.json({
users: [
{ id: 1, name: 'Test User' },
{ id: 2, name: 'Another User' },
],
})
);
}),
rest.post('/api/users', async (req, res, ctx) => {
const body = await req.json();
return res(
ctx.status(201),
ctx.json({ id: 3, ...body })
);
}),
];
Couverture de test et qualité
Générer des rapports de couverture
# Jest
npx jest --coverage
# Vitest
npx vitest --coverage
# Voir dans Cursor
# Cliquez sur le lien du rapport HTML dans le terminal
Analyse de couverture par Cursor
Analyse cette suite de tests :
1. Quels cas limites sont couverts par les tests ?
2. Quelles lacunes de test existent ?
3. Comment améliorer la couverture de branches ?
Configuration de l'intégration continue
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 run test:unit
- run: npm run test:integration
- run: npm run test:e2e
Référence rapide des tests
| Framework | Usage | Prompt Cursor |
|---|---|---|
| Jest | Unités/Intégration | "Écris un test Jest pour ce composant" |
| Vitest | Tests unitaires rapides | "Génère des tests Vitest couvrant les cas limites" |
| Playwright | Navigateur E2E | "Crée un test Playwright pour le flux de connexion" |
| Cypress | Navigateur E2E | "Écris un test Cypress pour la soumission de formulaire" |
| MSW | Mocking d'API | "Configure les handlers MSW pour l'API utilisateur" |
| React Testing Library | Tests de composants | "Teste ce composant React avec RTL" |
Patterns de test courants
| Pattern | Exemple |
|---|---|
| Fonctions factory | createUser({ role: 'admin' }) |
| Setup/Teardown | beforeEach / afterEach |
| Tests paramétrés | it.each([[1, 2, 3], [4, 5, 9]]) |
| Tests snapshot | expect(tree).toMatchSnapshot() |
| Spies | jest.spyOn(console, 'error') |
Ressources connexes
Dernière mise à jour : Juillet 2025