Testing e Qualità del Codice con Copilot
I test sono il fondamento di un progetto manutenibile. Non sono solo una rete di sicurezza contro i bug, ma anche documentazione eseguibile che descrive come il codice dovrebbe comportarsi. GitHub Copilot può accelerare drasticamente la scrittura di test, dal TDD al refactoring guidato, dal debugging assistito alla copertura dei casi edge.
In questo articolo esploreremo come sfruttare Copilot per ogni livello della piramide dei test, con prompt specifici, esempi pratici e best practices per ottenere test robusti e manutenibili.
📚 Panoramica della Serie
| # | Articolo | Focus |
|---|---|---|
| 1 | Foundation e Mindset | Setup e mentalità |
| 2 | Ideazione e Requisiti | Da idea a MVP |
| 3 | Architettura Backend | API e database |
| 4 | Struttura Frontend | UI e componenti |
| 5 | Prompt Engineering | Prompt e Agenti MCP |
| 6 | 📍 Sei qui → Testing e Qualità | Unit, integration, E2E |
| 7 | Documentazione | README, API docs, ADR |
| 8 | Deploy e DevOps | Docker, CI/CD |
| 9 | Evoluzione | Scalabilità e manutenzione |
La Piramide dei Test
La piramide dei test definisce la strategia ottimale per bilanciare velocità, copertura e manutenibilità. Copilot può aiutarti a tutti i livelli.
🔺 Piramide dei Test
╱╲
╱ ╲
╱ E2E╲ ← Pochi (5-10%)
╱──────╲ Lenti, costosi, fragili
╱ ╲ Testano flussi utente completi
╱Integration╲ ← Moderati (15-25%)
╱────────────╲ Velocità media
╱ ╲ Testano interazioni tra componenti
╱ Unit Tests ╲ ← Molti (70-80%)
╱──────────────────╲ Veloci, isolati, stabili
╱ ╲ Testano singole unità
╱______________________╲
📊 Caratteristiche per Livello
| Livello | Velocità | Isolamento | Costo | Confidenza |
|---|---|---|---|---|
| Unit | ~1ms | Alto | Basso | Logica interna |
| Integration | ~100ms | Medio | Medio | Interazioni |
| E2E | ~5s | Basso | Alto | Sistema completo |
Unit Test: La Base della Piramide
Gli unit test verificano singole funzioni o classi in isolamento completo. Sono veloci, affidabili e forniscono feedback immediato durante lo sviluppo.
Prompt per Generare Unit Test
Generate comprehensive unit tests for this service:
```typescript
[PASTE SERVICE CODE HERE]
```
TESTING REQUIREMENTS:
- Framework: Jest with TypeScript
- Coverage target: >90% for this critical service
- Mock ALL external dependencies (repositories, external services)
TEST CATEGORIES TO COVER:
1. Happy Path: Normal successful operations
2. Validation: Invalid inputs, edge cases
3. Error Handling: Expected errors are thrown correctly
4. Edge Cases: Empty arrays, null values, boundary conditions
5. State Changes: Verify side effects (calls to dependencies)
OUTPUT FORMAT:
- Complete test file with all imports
- Use describe/it blocks with clear descriptions
- Include beforeEach for setup
- Group tests by method
- Use AAA pattern (Arrange, Act, Assert)
- Add comments explaining non-obvious test cases
NAMING CONVENTION:
- Describe: "ServiceName"
- Nested describe: "methodName"
- It: "should [expected behavior] when [condition]"
Esempio Completo: UserService Unit Tests
import {{ '{' }} UserService {{ '}' }} from './user.service';
import {{ '{' }} UserRepository {{ '}' }} from './user.repository';
import {{ '{' }} EmailService {{ '}' }} from '@shared/services/email.service';
import {{ '{' }} ValidationError, NotFoundError, ConflictError {{ '}' }} from '@shared/errors';
import {{ '{' }} createUserFixture, validUserDto {{ '}' }} from './fixtures/user.fixtures';
describe('UserService', () => {{ '{' }}
let service: UserService;
let mockUserRepository: jest.Mocked<UserRepository>;
let mockEmailService: jest.Mocked<EmailService>;
beforeEach(() => {{ '{' }}
// Create fresh mocks for each test
mockUserRepository = {{ '{' }}
findById: jest.fn(),
findByEmail: jest.fn(),
create: jest.fn(),
update: jest.fn(),
softDelete: jest.fn(),
findAll: jest.fn(),
{{ '}' }} as any;
mockEmailService = {{ '{' }}
sendWelcomeEmail: jest.fn(),
sendPasswordResetEmail: jest.fn(),
{{ '}' }} as any;
service = new UserService(mockUserRepository, mockEmailService);
{{ '}' }});
afterEach(() => {{ '{' }}
jest.clearAllMocks();
{{ '}' }});
// ═══════════════════════════════════════════════════════════
// CREATE USER
// ═══════════════════════════════════════════════════════════
describe('createUser', () => {{ '{' }}
describe('happy path', () => {{ '{' }}
it('should create user and send welcome email when valid data provided', async () => {{ '{' }}
// Arrange
const dto = validUserDto;
const expectedUser = createUserFixture({{ '{' }} id: 'user-123', ...dto {{ '}' }});
mockUserRepository.findByEmail.mockResolvedValue(null);
mockUserRepository.create.mockResolvedValue(expectedUser);
mockEmailService.sendWelcomeEmail.mockResolvedValue(undefined);
// Act
const result = await service.createUser(dto);
// Assert
expect(result.id).toBe('user-123');
expect(result.email).toBe(dto.email);
expect(mockUserRepository.create).toHaveBeenCalledWith(
expect.objectContaining({{ '{' }}
name: dto.name,
email: dto.email,
// Password should be hashed, not plain text
password: expect.not.stringContaining(dto.password),
{{ '}' }})
);
expect(mockEmailService.sendWelcomeEmail).toHaveBeenCalledWith(
expectedUser.email,
expectedUser.name
);
{{ '}' }});
it('should hash password before saving', async () => {{ '{' }}
// Arrange
const dto = {{ '{' }} ...validUserDto, password: 'PlainPassword123!' {{ '}' }};
mockUserRepository.findByEmail.mockResolvedValue(null);
mockUserRepository.create.mockResolvedValue(createUserFixture());
// Act
await service.createUser(dto);
// Assert
const createCall = mockUserRepository.create.mock.calls[0][0];
expect(createCall.password).not.toBe('PlainPassword123!');
expect(createCall.password.length).toBeGreaterThan(50); // bcrypt hash length
{{ '}' }});
{{ '}' }});
describe('validation errors', () => {{ '{' }}
it('should throw ConflictError when email already exists', async () => {{ '{' }}
// Arrange
const existingUser = createUserFixture({{ '{' }} email: 'exists@test.com' {{ '}' }});
mockUserRepository.findByEmail.mockResolvedValue(existingUser);
// Act & Assert
await expect(
service.createUser({{ '{' }} ...validUserDto, email: 'exists@test.com' {{ '}' }})
).rejects.toThrow(ConflictError);
expect(mockUserRepository.create).not.toHaveBeenCalled();
expect(mockEmailService.sendWelcomeEmail).not.toHaveBeenCalled();
{{ '}' }});
it('should throw ValidationError when password is too weak', async () => {{ '{' }}
// Arrange
mockUserRepository.findByEmail.mockResolvedValue(null);
// Act & Assert
await expect(
service.createUser({{ '{' }} ...validUserDto, password: '123' {{ '}' }})
).rejects.toThrow(ValidationError);
{{ '}' }});
it('should throw ValidationError when email format is invalid', async () => {{ '{' }}
await expect(
service.createUser({{ '{' }} ...validUserDto, email: 'not-an-email' {{ '}' }})
).rejects.toThrow(ValidationError);
{{ '}' }});
{{ '}' }});
describe('edge cases', () => {{ '{' }}
it('should trim whitespace from email', async () => {{ '{' }}
// Arrange
mockUserRepository.findByEmail.mockResolvedValue(null);
mockUserRepository.create.mockResolvedValue(createUserFixture());
// Act
await service.createUser({{ '{' }}
...validUserDto,
email: ' user@test.com '
{{ '}' }});
// Assert
expect(mockUserRepository.findByEmail).toHaveBeenCalledWith('user@test.com');
{{ '}' }});
it('should normalize email to lowercase', async () => {{ '{' }}
// Arrange
mockUserRepository.findByEmail.mockResolvedValue(null);
mockUserRepository.create.mockResolvedValue(createUserFixture());
// Act
await service.createUser({{ '{' }}
...validUserDto,
email: 'User@TEST.com'
{{ '}' }});
// Assert
expect(mockUserRepository.findByEmail).toHaveBeenCalledWith('user@test.com');
{{ '}' }});
{{ '}' }});
{{ '}' }});
// ═══════════════════════════════════════════════════════════
// GET USER BY ID
// ═══════════════════════════════════════════════════════════
describe('getUserById', () => {{ '{' }}
it('should return user when found', async () => {{ '{' }}
// Arrange
const user = createUserFixture({{ '{' }} id: 'user-123' {{ '}' }});
mockUserRepository.findById.mockResolvedValue(user);
// Act
const result = await service.getUserById('user-123');
// Assert
expect(result).toEqual(user);
expect(mockUserRepository.findById).toHaveBeenCalledWith('user-123');
{{ '}' }});
it('should throw NotFoundError when user does not exist', async () => {{ '{' }}
// Arrange
mockUserRepository.findById.mockResolvedValue(null);
// Act & Assert
await expect(service.getUserById('non-existent'))
.rejects.toThrow(NotFoundError);
{{ '}' }});
it('should throw ValidationError when id is empty', async () => {{ '{' }}
await expect(service.getUserById('')).rejects.toThrow(ValidationError);
{{ '}' }});
{{ '}' }});
// ═══════════════════════════════════════════════════════════
// UPDATE USER
// ═══════════════════════════════════════════════════════════
describe('updateUser', () => {{ '{' }}
it('should update user fields', async () => {{ '{' }}
// Arrange
const existingUser = createUserFixture({{ '{' }} id: 'user-123', name: 'Old Name' {{ '}' }});
const updatedUser = {{ '{' }} ...existingUser, name: 'New Name' {{ '}' }};
mockUserRepository.findById.mockResolvedValue(existingUser);
mockUserRepository.update.mockResolvedValue(updatedUser);
// Act
const result = await service.updateUser('user-123', {{ '{' }} name: 'New Name' {{ '}' }});
// Assert
expect(result.name).toBe('New Name');
expect(mockUserRepository.update).toHaveBeenCalledWith('user-123', {{ '{' }} name: 'New Name' {{ '}' }});
{{ '}' }});
it('should throw ConflictError when updating to existing email', async () => {{ '{' }}
// Arrange
const existingUser = createUserFixture({{ '{' }} id: 'user-123' {{ '}' }});
const otherUser = createUserFixture({{ '{' }} id: 'user-456', email: 'taken@test.com' {{ '}' }});
mockUserRepository.findById.mockResolvedValue(existingUser);
mockUserRepository.findByEmail.mockResolvedValue(otherUser);
// Act & Assert
await expect(
service.updateUser('user-123', {{ '{' }} email: 'taken@test.com' {{ '}' }})
).rejects.toThrow(ConflictError);
{{ '}' }});
it('should allow updating to same email (no change)', async () => {{ '{' }}
// Arrange
const existingUser = createUserFixture({{ '{' }} id: 'user-123', email: 'same@test.com' {{ '}' }});
mockUserRepository.findById.mockResolvedValue(existingUser);
mockUserRepository.findByEmail.mockResolvedValue(existingUser); // Same user
mockUserRepository.update.mockResolvedValue(existingUser);
// Act & Assert - should not throw
await expect(
service.updateUser('user-123', {{ '{' }} email: 'same@test.com' {{ '}' }})
).resolves.not.toThrow();
{{ '}' }});
{{ '}' }});
// ═══════════════════════════════════════════════════════════
// SOFT DELETE USER
// ═══════════════════════════════════════════════════════════
describe('softDeleteUser', () => {{ '{' }}
it('should soft delete existing user', async () => {{ '{' }}
// Arrange
const user = createUserFixture({{ '{' }} id: 'user-123' {{ '}' }});
mockUserRepository.findById.mockResolvedValue(user);
mockUserRepository.softDelete.mockResolvedValue(undefined);
// Act
await service.softDeleteUser('user-123');
// Assert
expect(mockUserRepository.softDelete).toHaveBeenCalledWith('user-123');
{{ '}' }});
it('should throw NotFoundError when user does not exist', async () => {{ '{' }}
// Arrange
mockUserRepository.findById.mockResolvedValue(null);
// Act & Assert
await expect(service.softDeleteUser('non-existent'))
.rejects.toThrow(NotFoundError);
expect(mockUserRepository.softDelete).not.toHaveBeenCalled();
{{ '}' }});
{{ '}' }});
{{ '}' }});
Integration Tests: Testare le Interazioni
Gli integration test verificano che i componenti funzionino correttamente insieme. Testano l'API completa, incluso routing, middleware, validazione e accesso al database.
Prompt per Integration Tests
Generate integration tests for this API:
ENDPOINTS:
- POST /api/users → Create user (requires auth)
- GET /api/users/:id → Get user by ID
- PATCH /api/users/:id → Update user (owner only)
- DELETE /api/users/:id → Soft delete (admin only)
TESTING REQUIREMENTS:
- Framework: Jest + Supertest
- Database: Test PostgreSQL (use transactions, rollback after each test)
- Auth: JWT tokens (mock or real test tokens)
TEST SCENARIOS FOR EACH ENDPOINT:
1. Success case with valid request
2. Validation errors (400) - invalid body, missing fields
3. Authentication errors (401) - missing/invalid token
4. Authorization errors (403) - wrong role/permissions
5. Not found errors (404) - resource doesn't exist
6. Conflict errors (409) - duplicate resources
SETUP/TEARDOWN:
- beforeAll: Create test database, run migrations
- beforeEach: Start transaction
- afterEach: Rollback transaction
- afterAll: Close database connection
Include helper functions for:
- Creating authenticated requests
- Generating test users
- Cleaning up test data
Esempio Completo: Users API Integration Tests
import request from 'supertest';
import {{ '{' }} app {{ '}' }} from '../app';
import {{ '{' }} db {{ '}' }} from '../database';
import {{ '{' }} createTestUser, getAuthToken, cleanupTestData {{ '}' }} from './helpers';
import {{ '{' }} UserRole {{ '}' }} from '@shared/types';
describe('Users API Integration Tests', () => {{ '{' }}
let adminToken: string;
let userToken: string;
let testUserId: string;
beforeAll(async () => {{ '{' }}
await db.connect();
await db.migrate();
// Create test users and get tokens
const admin = await createTestUser({{ '{' }} role: UserRole.ADMIN {{ '}' }});
const user = await createTestUser({{ '{' }} role: UserRole.USER {{ '}' }});
adminToken = await getAuthToken(admin);
userToken = await getAuthToken(user);
testUserId = user.id;
{{ '}' }});
afterAll(async () => {{ '{' }}
await cleanupTestData();
await db.disconnect();
{{ '}' }});
// ═══════════════════════════════════════════════════════════
// POST /api/users - Create User
// ═══════════════════════════════════════════════════════════
describe('POST /api/users', () => {{ '{' }}
const validPayload = {{ '{' }}
name: 'Integration Test User',
email: 'integration@test.com',
password: 'SecurePass123!',
{{ '}' }};
describe('success cases', () => {{ '{' }}
it('should create user with valid data (201)', async () => {{ '{' }}
const response = await request(app)
.post('/api/users')
.set('Authorization', `Bearer ${{ '{' }}adminToken{{ '}' }}`)
.send({{ '{' }} ...validPayload, email: `unique-${{ '{' }}Date.now(){{ '}' }}@test.com` {{ '}' }});
expect(response.status).toBe(201);
expect(response.body.data).toMatchObject({{ '{' }}
name: validPayload.name,
email: expect.stringContaining('@test.com'),
{{ '}' }});
expect(response.body.data.password).toBeUndefined(); // Never return password
expect(response.body.data.id).toBeDefined();
{{ '}' }});
{{ '}' }});
describe('validation errors (400)', () => {{ '{' }}
it('should return 400 when email is invalid', async () => {{ '{' }}
const response = await request(app)
.post('/api/users')
.set('Authorization', `Bearer ${{ '{' }}adminToken{{ '}' }}`)
.send({{ '{' }} ...validPayload, email: 'not-an-email' {{ '}' }});
expect(response.status).toBe(400);
expect(response.body.error.code).toBe('VALIDATION_ERROR');
expect(response.body.error.details).toContainEqual(
expect.objectContaining({{ '{' }} field: 'email' {{ '}' }})
);
{{ '}' }});
it('should return 400 when password is too weak', async () => {{ '{' }}
const response = await request(app)
.post('/api/users')
.set('Authorization', `Bearer ${{ '{' }}adminToken{{ '}' }}`)
.send({{ '{' }} ...validPayload, password: '123' {{ '}' }});
expect(response.status).toBe(400);
expect(response.body.error.details).toContainEqual(
expect.objectContaining({{ '{' }} field: 'password' {{ '}' }})
);
{{ '}' }});
it('should return 400 when required fields are missing', async () => {{ '{' }}
const response = await request(app)
.post('/api/users')
.set('Authorization', `Bearer ${{ '{' }}adminToken{{ '}' }}`)
.send({{ '{' }}{{ '}' }}); // Empty body
expect(response.status).toBe(400);
{{ '}' }});
{{ '}' }});
describe('authentication errors (401)', () => {{ '{' }}
it('should return 401 without auth token', async () => {{ '{' }}
const response = await request(app)
.post('/api/users')
.send(validPayload);
expect(response.status).toBe(401);
expect(response.body.error.code).toBe('UNAUTHORIZED');
{{ '}' }});
it('should return 401 with invalid token', async () => {{ '{' }}
const response = await request(app)
.post('/api/users')
.set('Authorization', 'Bearer invalid-token')
.send(validPayload);
expect(response.status).toBe(401);
{{ '}' }});
it('should return 401 with expired token', async () => {{ '{' }}
const expiredToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'; // Expired
const response = await request(app)
.post('/api/users')
.set('Authorization', `Bearer ${{ '{' }}expiredToken{{ '}' }}`)
.send(validPayload);
expect(response.status).toBe(401);
{{ '}' }});
{{ '}' }});
describe('conflict errors (409)', () => {{ '{' }}
it('should return 409 when email already exists', async () => {{ '{' }}
// First, create a user
const email = `conflict-${{ '{' }}Date.now(){{ '}' }}@test.com`;
await request(app)
.post('/api/users')
.set('Authorization', `Bearer ${{ '{' }}adminToken{{ '}' }}`)
.send({{ '{' }} ...validPayload, email {{ '}' }});
// Try to create with same email
const response = await request(app)
.post('/api/users')
.set('Authorization', `Bearer ${{ '{' }}adminToken{{ '}' }}`)
.send({{ '{' }} ...validPayload, email {{ '}' }});
expect(response.status).toBe(409);
expect(response.body.error.code).toBe('CONFLICT');
{{ '}' }});
{{ '}' }});
{{ '}' }});
// ═══════════════════════════════════════════════════════════
// GET /api/users/:id - Get User by ID
// ═══════════════════════════════════════════════════════════
describe('GET /api/users/:id', () => {{ '{' }}
it('should return user when found (200)', async () => {{ '{' }}
const response = await request(app)
.get(`/api/users/${{ '{' }}testUserId{{ '}' }}`)
.set('Authorization', `Bearer ${{ '{' }}userToken{{ '}' }}`);
expect(response.status).toBe(200);
expect(response.body.data.id).toBe(testUserId);
expect(response.body.data.password).toBeUndefined();
{{ '}' }});
it('should return 404 when user not found', async () => {{ '{' }}
const response = await request(app)
.get('/api/users/non-existent-id')
.set('Authorization', `Bearer ${{ '{' }}userToken{{ '}' }}`);
expect(response.status).toBe(404);
expect(response.body.error.code).toBe('NOT_FOUND');
{{ '}' }});
it('should return 400 for invalid UUID format', async () => {{ '{' }}
const response = await request(app)
.get('/api/users/not-a-uuid')
.set('Authorization', `Bearer ${{ '{' }}userToken{{ '}' }}`);
expect(response.status).toBe(400);
{{ '}' }});
{{ '}' }});
// ═══════════════════════════════════════════════════════════
// DELETE /api/users/:id - Soft Delete (Admin Only)
// ═══════════════════════════════════════════════════════════
describe('DELETE /api/users/:id', () => {{ '{' }}
it('should soft delete user when admin (204)', async () => {{ '{' }}
// Create user to delete
const userToDelete = await createTestUser();
const response = await request(app)
.delete(`/api/users/${{ '{' }}userToDelete.id{{ '}' }}`)
.set('Authorization', `Bearer ${{ '{' }}adminToken{{ '}' }}`);
expect(response.status).toBe(204);
// Verify user is soft deleted (can't fetch)
const getResponse = await request(app)
.get(`/api/users/${{ '{' }}userToDelete.id{{ '}' }}`)
.set('Authorization', `Bearer ${{ '{' }}adminToken{{ '}' }}`);
expect(getResponse.status).toBe(404);
{{ '}' }});
it('should return 403 when non-admin tries to delete', async () => {{ '{' }}
const response = await request(app)
.delete(`/api/users/${{ '{' }}testUserId{{ '}' }}`)
.set('Authorization', `Bearer ${{ '{' }}userToken{{ '}' }}`);
expect(response.status).toBe(403);
expect(response.body.error.code).toBe('FORBIDDEN');
{{ '}' }});
{{ '}' }});
{{ '}' }});
E2E Tests: Flussi Utente Completi
I test end-to-end simulano il comportamento reale dell'utente, testando l'intera applicazione dal browser al database.
Prompt per E2E Tests
Generate E2E tests for user registration and login flow:
FLOW TO TEST:
1. User visits homepage
2. Clicks "Sign Up" button
3. Fills registration form (name, email, password)
4. Submits form
5. Sees success message
6. Is redirected to dashboard
7. Can log out and log back in
FRAMEWORK: Playwright with TypeScript
TEST SCENARIOS:
1. Happy path: Complete registration and login
2. Validation: Form shows errors for invalid input
3. Duplicate email: Shows appropriate error
4. Login with wrong password: Shows error
5. Remember me: Session persists after browser close
INCLUDE:
- Page Object Model for reusable selectors
- Helper functions for common actions
- Screenshot on failure
- Retry logic for flaky network requests
Esempio E2E con Playwright
import {{ '{' }} test, expect {{ '}' }} from '@playwright/test';
import {{ '{' }} LoginPage {{ '}' }} from './pages/login.page';
import {{ '{' }} RegisterPage {{ '}' }} from './pages/register.page';
import {{ '{' }} DashboardPage {{ '}' }} from './pages/dashboard.page';
import {{ '{' }} generateTestEmail {{ '}' }} from './helpers';
test.describe('User Authentication Flow', () => {{ '{' }}
test.describe('Registration', () => {{ '{' }}
test('should complete registration successfully', async ({{ '{' }} page {{ '}' }}) => {{ '{' }}
const registerPage = new RegisterPage(page);
const dashboardPage = new DashboardPage(page);
const testEmail = generateTestEmail();
// Navigate to registration
await registerPage.goto();
await expect(page).toHaveTitle(/Sign Up/);
// Fill form
await registerPage.fillName('Test User');
await registerPage.fillEmail(testEmail);
await registerPage.fillPassword('SecurePass123!');
await registerPage.fillConfirmPassword('SecurePass123!');
// Submit and verify
await registerPage.submit();
// Should redirect to dashboard
await expect(page).toHaveURL(/\/dashboard/);
await expect(dashboardPage.welcomeMessage).toContainText('Welcome, Test User');
{{ '}' }});
test('should show validation errors for invalid input', async ({{ '{' }} page {{ '}' }}) => {{ '{' }}
const registerPage = new RegisterPage(page);
await registerPage.goto();
// Submit empty form
await registerPage.submit();
// Check validation messages
await expect(registerPage.nameError).toBeVisible();
await expect(registerPage.emailError).toBeVisible();
await expect(registerPage.passwordError).toBeVisible();
{{ '}' }});
test('should show error for duplicate email', async ({{ '{' }} page {{ '}' }}) => {{ '{' }}
const registerPage = new RegisterPage(page);
await registerPage.goto();
await registerPage.fillName('Duplicate User');
await registerPage.fillEmail('existing@test.com'); // Pre-seeded
await registerPage.fillPassword('SecurePass123!');
await registerPage.fillConfirmPassword('SecurePass123!');
await registerPage.submit();
await expect(registerPage.formError).toContainText('Email already registered');
{{ '}' }});
{{ '}' }});
test.describe('Login', () => {{ '{' }}
test('should login with valid credentials', async ({{ '{' }} page {{ '}' }}) => {{ '{' }}
const loginPage = new LoginPage(page);
const dashboardPage = new DashboardPage(page);
await loginPage.goto();
await loginPage.fillEmail('test@example.com');
await loginPage.fillPassword('TestPass123!');
await loginPage.submit();
await expect(page).toHaveURL(/\/dashboard/);
await expect(dashboardPage.userMenu).toBeVisible();
{{ '}' }});
test('should show error for invalid credentials', async ({{ '{' }} page {{ '}' }}) => {{ '{' }}
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.fillEmail('test@example.com');
await loginPage.fillPassword('WrongPassword');
await loginPage.submit();
await expect(loginPage.errorMessage).toContainText('Invalid credentials');
await expect(page).toHaveURL(/\/login/); // Still on login page
{{ '}' }});
{{ '}' }});
test.describe('Session Management', () => {{ '{' }}
test('should persist session with remember me', async ({{ '{' }} page, context {{ '}' }}) => {{ '{' }}
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.fillEmail('test@example.com');
await loginPage.fillPassword('TestPass123!');
await loginPage.checkRememberMe();
await loginPage.submit();
// Close and reopen browser
await context.clearCookies();
const cookies = await context.cookies();
await context.addCookies(cookies);
// Navigate to protected page
await page.goto('/dashboard');
// Should still be logged in
await expect(page).toHaveURL(/\/dashboard/);
{{ '}' }});
{{ '}' }});
{{ '}' }});
TDD Workflow con Copilot
Il Test-Driven Development (TDD) funziona ancora meglio con Copilot. Scrivi prima il test, poi lascia che Copilot suggerisca l'implementazione.
🔄 TDD con Copilot
| Step | Tu | Copilot |
|---|---|---|
| 1. Red | Scrivi test che fallisce | Suggerisce casi test |
| 2. Green | Chiedi implementazione | Genera codice minimo |
| 3. Refactor | Chiedi miglioramenti | Propone refactoring |
| 4. Repeat | Aggiungi nuovo test | Suggerisce edge cases |
I'm following TDD. Here's my failing test:
```typescript
describe('calculateDiscount', () => {{ '{' }}
it('should apply 10% discount for orders over $100', () => {{ '{' }}
expect(calculateDiscount(150)).toBe(15);
{{ '}' }});
it('should apply 20% discount for orders over $500', () => {{ '{' }}
expect(calculateDiscount(600)).toBe(120);
{{ '}' }});
it('should return 0 for orders under $100', () => {{ '{' }}
expect(calculateDiscount(50)).toBe(0);
{{ '}' }});
{{ '}' }});
```
1. Implement the MINIMUM code to make these tests pass
2. Don't add features not covered by tests
3. After implementation, suggest additional edge cases I should test
Test Coverage e Gap Analysis
Usa Copilot per identificare codice non coperto e generare test mancanti.
My coverage report shows uncovered lines:
FILE: src/services/order.service.ts
UNCOVERED LINES: 45-52, 78-85, 120-135
Here's the code at those lines:
```typescript
// Lines 45-52
if (order.status === 'cancelled') {{ '{' }}
throw new ValidationError('Cannot modify cancelled order');
{{ '}' }}
// Lines 78-85
if (items.some(item => item.quantity > item.stock)) {{ '{' }}
const outOfStock = items.filter(i => i.quantity > i.stock);
throw new ValidationError(`Insufficient stock for: ${{ '{' }}outOfStock.map(i => i.name).join(', '){{ '}' }}`);
{{ '}' }}
// Lines 120-135
if (coupon && coupon.expiresAt < new Date()) {{ '{' }}
throw new ValidationError('Coupon has expired');
{{ '}' }}
if (coupon && coupon.usageCount >= coupon.maxUsage) {{ '{' }}
throw new ValidationError('Coupon usage limit reached');
{{ '}' }}
```
Generate unit tests to cover these scenarios. Include:
1. Test for each branch
2. Boundary conditions
3. Descriptive test names
Test Fixtures e Factories
Fixture ben strutturate rendono i test più leggibili e manutenibili.
// fixtures/user.fixtures.ts
import {{ '{' }} User, UserRole, UserStatus {{ '}' }} from '@/entities/user.entity';
import {{ '{' }} CreateUserDto {{ '}' }} from '@/dto/create-user.dto';
// ═══════════════════════════════════════════════════════════
// BASE FIXTURES - Valid default values
// ═══════════════════════════════════════════════════════════
export const validUserDto: CreateUserDto = {{ '{' }}
name: 'Test User',
email: 'test@example.com',
password: 'SecurePassword123!',
{{ '}' }};
export const validUser: Partial<User> = {{ '{' }}
id: 'user-123',
name: 'Test User',
email: 'test@example.com',
role: UserRole.USER,
status: UserStatus.ACTIVE,
createdAt: new Date('2025-01-01'),
updatedAt: new Date('2025-01-01'),
{{ '}' }};
// ═══════════════════════════════════════════════════════════
// FACTORY FUNCTION - Create users with overrides
// ═══════════════════════════════════════════════════════════
let userCounter = 0;
export function createUserFixture(overrides: Partial<User> = {{ '{' }}{{ '}' }}): User {{ '{' }}
userCounter++;
return {{ '{' }}
id: `user-${{ '{' }}userCounter{{ '}' }}`,
name: 'Test User',
email: `test-${{ '{' }}userCounter{{ '}' }}@example.com`,
password: '$2b$10$hashedpassword', // Pre-hashed for tests
role: UserRole.USER,
status: UserStatus.ACTIVE,
createdAt: new Date(),
updatedAt: new Date(),
deletedAt: null,
...overrides,
{{ '}' }} as User;
{{ '}' }}
// ═══════════════════════════════════════════════════════════
// INVALID FIXTURES - For testing validation
// ═══════════════════════════════════════════════════════════
export const invalidUserDtos = {{ '{' }}
missingName: {{ '{' }}
email: 'test@example.com',
password: 'SecurePassword123!',
{{ '}' }},
missingEmail: {{ '{' }}
name: 'Test User',
password: 'SecurePassword123!',
{{ '}' }},
invalidEmail: {{ '{' }}
name: 'Test User',
email: 'not-an-email',
password: 'SecurePassword123!',
{{ '}' }},
weakPassword: {{ '{' }}
name: 'Test User',
email: 'test@example.com',
password: '123',
{{ '}' }},
emptyName: {{ '{' }}
name: '',
email: 'test@example.com',
password: 'SecurePassword123!',
{{ '}' }},
{{ '}' }};
// ═══════════════════════════════════════════════════════════
// SCENARIO FIXTURES - Specific test scenarios
// ═══════════════════════════════════════════════════════════
export const userScenarios = {{ '{' }}
admin: () => createUserFixture({{ '{' }} role: UserRole.ADMIN {{ '}' }}),
inactive: () => createUserFixture({{ '{' }} status: UserStatus.INACTIVE {{ '}' }}),
deleted: () => createUserFixture({{ '{' }}
status: UserStatus.DELETED,
deletedAt: new Date(),
{{ '}' }}),
premium: () => createUserFixture({{ '{' }}
role: UserRole.PREMIUM,
premiumExpiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
{{ '}' }}),
{{ '}' }};
// ═══════════════════════════════════════════════════════════
// HELPERS - Reset counter for test isolation
// ═══════════════════════════════════════════════════════════
export function resetUserFixtures(): void {{ '{' }}
userCounter = 0;
{{ '}' }}
Refactoring Guidato con Test
I test esistenti ti permettono di refactoring in sicurezza. Copilot può suggerire miglioramenti mantenendo il comportamento.
I want to refactor this code. I have tests that cover its behavior:
CURRENT CODE:
```typescript
[PASTE CODE]
```
EXISTING TESTS (passing):
```typescript
[PASTE TESTS]
```
REFACTORING GOALS:
1. Reduce cyclomatic complexity
2. Extract reusable functions
3. Improve naming
CONSTRAINTS:
- All existing tests MUST still pass
- Don't change public interface
- No new dependencies
Provide:
1. Refactored code
2. Explanation of each change
3. Any new tests needed for extracted functions
Debug Assistito con Test
This test is failing and I can't figure out why:
TEST:
```typescript
it('should apply discount to order total', async () => {{ '{' }}
const order = await orderService.createOrder({{ '{' }}
items: [{{ '{' }} productId: 'prod-1', quantity: 2 {{ '}' }}],
couponCode: 'SAVE10',
{{ '}' }});
expect(order.total).toBe(90); // Expected $90 after 10% discount
expect(order.discount).toBe(10);
{{ '}' }});
```
ERROR:
```
Expected: 90
Received: 100
```
IMPLEMENTATION:
```typescript
async createOrder(dto: CreateOrderDto): Promise<Order> {{ '{' }}
const items = await this.getOrderItems(dto.items);
const subtotal = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
let discount = 0;
if (dto.couponCode) {{ '{' }}
const coupon = await this.couponRepository.findByCode(dto.couponCode);
if (coupon) {{ '{' }}
discount = subtotal * (coupon.percentage / 100);
{{ '}' }}
{{ '}' }}
return this.orderRepository.create({{ '{' }}
items,
subtotal,
discount,
total: subtotal - discount,
{{ '}' }});
{{ '}' }}
```
Help me:
1. Identify why the test is failing
2. Determine if it's a test bug or implementation bug
3. Provide the fix
Testing Best Practices
❌ Anti-Pattern
- Test che dipendono dall'ordine
- Stato condiviso tra test
- Nomi vaghi: "should work"
- Multiple assertions non correlate
- Test troppo granulari
- Mock eccessivo (testing mocks)
- Test lenti senza motivo
- Ignorare test flaky
✅ Best Practices
- Test indipendenti e isolati
- Setup fresh per ogni test
- Nomi descrittivi: "should return 404 when user not found"
- Una asserzione logica per test
- Test behavior, non implementation
- Mock solo le dipendenze esterne
- Ottimizza setup condiviso
- Fix o rimuovi test flaky
Checklist Qualità dei Test
✅ Prima di Fare Merge
- ☐ Tutti i test passano localmente
- ☐ Coverage >= 80% per codice critico
- ☐ Nessun test flaky (esegui 3 volte)
- ☐ Test names sono descrittivi
- ☐ Edge cases sono coperti
- ☐ Error paths sono testati
- ☐ Nessun console.log nei test
- ☐ Fixtures sono pulite e riutilizzabili
- ☐ Mock sono realistici
- ☐ Test sono veloci (unit < 10s total)
Conclusione e Prossimi Passi
Il testing è dove Copilot brilla davvero. Può generare test completi partendo dal codice esistente, suggerire edge cases che potresti non aver considerato, e aiutarti a mantenere alta la coverage senza sacrificare la velocità di sviluppo.
Ricorda: i test generati da Copilot devono sempre essere verificati. L'AI può generare test che passano ma non testano realmente il comportamento importante, o che sono troppo legati all'implementazione invece che al comportamento.
Nel prossimo articolo vedremo come usare Copilot per la documentazione professionale: README completi, API documentation con OpenAPI, Architecture Decision Records e JSDoc.
🎯 Punti Chiave da Ricordare
- Piramide: 70% unit, 20% integration, 10% E2E
- Unit: Isolamento completo, mock dipendenze
- Integration: Testa l'API end-to-end con DB reale
- E2E: Simula il comportamento utente reale
- TDD: Scrivi test prima, implementa dopo
- Coverage: Mira a 80%+ per codice critico
- Fixtures: Factory functions per dati riutilizzabili
- Verifica: Sempre controllare i test generati

Commenti
Caricamento commenti...
Accedi per commentare