Comprehensive guide for writing fast, maintainable tests in this project.
npm installnpm run test:setup# All tests (sequential, for CI)
npm test
# Fast parallel execution (for development)
npm run test:parallel
# Specific test suites
npm run test:unit # Unit tests only
npm run test:integration # Integration tests only
npm run test:fast # Fast unit tests in paralleltests/
├── unit/ # Unit tests (fast, isolated)
│ ├── services/ # Service layer tests
│ ├── middleware/ # Middleware tests
│ └── utils/ # Utility function tests
├── integration/ # Integration tests (with database)
│ ├── api/ # Public API tests
│ └── admin/ # Admin API tests
├── e2e/ # End-to-end tests (Playwright)
└── helpers/ # Test utilities
├── builders/ # Test data builders
├── customMatchers.js # Custom Jest matchers
├── database.js # Database helpers
├── fixtures.js # Test data fixtures
├── seeders.js # Database seeders
├── testDatabase.js # Shared connection pool
├── transactionHelper.js # Transaction isolation
└── performanceMonitor.js # Performance tracking
# Fast feedback loop - unit tests only
npm run test:fast
# Watch mode for TDD
npm run test:watch
# Specific test file
npm test -- appointments.test.js
# Specific test pattern
npm test -- --testNamePattern="should create appointment"# Sequential execution (stable for CI)
npm test
# With coverage report
npm run test:coverage
# All tests including E2E
npm run test:allnpm run test:unit # All unit tests
npm run test:integration # All integration tests
npm run test:admin # Admin API tests only
npm run test:api # Public API tests only
npm run test:services # Service unit tests
npm run test:e2e # E2E tests with PlaywrightBuild test data with a readable, chainable API.
const { AppointmentBuilder } = require('../helpers/builders');
// Simple appointment
const appointment = new AppointmentBuilder()
.withName('Γιάννης Παπαδόπουλος')
.onDate('2025-12-15')
.atTime('14:00:00')
.forTaxReturn()
.build();
// Random appointment
const randomAppointment = new AppointmentBuilder()
.onRandomFutureDate()
.atRandomTime()
.forConsultation()
.build();
// Bulk appointments
const appointments = new AppointmentBuilder().forBookkeeping().buildMany(10);const { AdminBuilder } = require('../helpers/builders');
const admin = new AdminBuilder()
.withUsername('admin')
.withPassword('SecurePass123!')
.withGreekEmail()
.build();Fast database population for tests.
const {
seedAdminUser,
seedAppointments,
seedFullyBookedDay,
seedBlockedDates,
} = require('../helpers/seeders');
// Create admin user (bypasses HTTP, much faster)
const admin = await seedAdminUser({
username: 'admin',
password: 'SecurePass123!',
email: 'admin@example.com',
});
// Seed 10 appointments
const appointmentIds = await seedAppointments(10);
// Create fully booked day
await seedFullyBookedDay('2025-12-20', '09:00:00', '17:00:00');
// Add blocked dates
await seedBlockedDates([
{ date: '2025-12-25', reason: 'Christmas', all_day: true },
{ date: '2025-01-01', reason: 'New Year', all_day: true },
]);Domain-specific assertions for clearer tests.
// Appointment validation
expect(appointment).toBeValidAppointment();
// Database assertions
expect(appointmentId).toExistInDatabase();
expect(appointmentId).toHaveStatusInDatabase('confirmed');
// API response assertions
expect(response).toIndicateSuccess();
expect(response).toIndicateError();
// Domain-specific validations
expect('6912345678').toBeValidGreekPhone();
expect('2025-12-15').toBeWorkingDay();
expect(appointment).toMatchAppointmentSchema();Fast test isolation using database transactions (10-20x faster than truncate).
const { withTransaction } = require('../helpers/transactionHelper');
test('should create appointment', async () => {
await withTransaction(async (tx) => {
// All queries within this block use the transaction
await tx.query('INSERT INTO appointments (...) VALUES (...)', []);
const [rows] = await tx.query('SELECT * FROM appointments WHERE id = ?', [1]);
expect(rows.length).toBe(1);
// Transaction automatically rolls back after test
});
});For entire test suites:
const { describeWithTransactions } = require('../helpers/transactionHelper');
describeWithTransactions('Appointment Service', () => {
// All tests automatically use transactions
test('should create appointment', async () => {
const db = getDb();
await db.query('INSERT INTO appointments (...) VALUES (...)', []);
// Automatically rolled back
});
});Automatically tracks and reports slow tests.
// Performance monitoring is enabled by default
// Disable with: DISABLE_PERF_MONITOR=true npm test
// Export performance data for analysis
const { exportPerformanceData } = require('../helpers/performanceMonitor');
exportPerformanceData('./test-performance.json');Performance Report Example:
📊 TEST PERFORMANCE REPORT
================================================================================
Total Tests: 105
Total Time: 224.34s
Average: 2137ms per test
Slowest: 63403ms
🔴 VERY SLOW TESTS (>3000ms):
1. 63403ms - Admin Appointments API › PUT /api/admin/appointments/:id
2. 46512ms - Admin Auth API › POST /api/admin/login
💡 OPTIMIZATION SUGGESTIONS:
• Admin tests are slow - consider using seedAdminUser() instead of HTTP
• Database tests are slow - consider transaction-based isolation
Efficient database connection management.
const { getTestDatabase } = require('../helpers/testDatabase');
beforeAll(async () => {
// Initialize shared pool once
await getTestDatabase();
});
// All tests share the same connection pool
// No need to create separate pools per file-
Use Builders for Test Data
// Good const appointment = new AppointmentBuilder().onDate('2025-12-15').forTaxReturn().build(); // Avoid const appointment = { client_name: 'John Doe', client_email: 'john@example.com', // ... hardcoded fields };
-
Use Seeders for Database Setup
// Good - Direct DB insert (fast) const admin = await seedAdminUser(); // Avoid - HTTP requests (slow) await request(app).post('/api/admin/setup').send({...});
-
Use Custom Matchers
// Good - Expressive expect(response).toIndicateSuccess(); expect(appointmentId).toExistInDatabase(); // Avoid - Verbose expect(response.body.success).toBe(true); const [rows] = await db.query('SELECT * FROM appointments WHERE id = ?', [id]); expect(rows.length).toBeGreaterThan(0);
-
Use Transactions for Unit Tests
// Good - Fast rollback await withTransaction(async (tx) => { await tx.query('INSERT ...'); // Automatically rolled back }); // Avoid - Slow truncate beforeEach(async () => { await clearTestDatabase(); });
-
Keep Tests Independent
// Good test('test 1', async () => { const appointment = await seedAppointments(1); // ... test logic }); // Avoid - Tests depend on execution order let sharedAppointmentId; test('test 1', async () => { sharedAppointmentId = await createAppointment(); }); test('test 2', async () => { await updateAppointment(sharedAppointmentId); // Depends on test 1! });
- Don't create redundant database connections
- Don't use HTTP for test setup when direct DB is faster
- Don't hardcode test data - use builders or faker
- Don't forget to clean up after tests
- Don't write slow tests without justification
- Benefit: Eliminates connection overhead (1-2s saved)
- Already implemented via
getTestDatabase()
- Benefit: 10-20x faster than full HTTP requests
- Example:
seedAdminUser()vs HTTP/api/admin/setup
- Benefit: Reduces bcrypt operations from 70+ to ~5
- Already implemented in admin test files
- Benefit: 10-20x faster than truncating tables
- Usage:
withTransaction()ordescribeWithTransactions()
# Development: Fast parallel execution
npm run test:parallel
# CI: Stable sequential execution
npm test# Run only fast tests during development
npm run test:fast
# Run specific test suites
npm run test:admin
npm run test:api| Optimization | Before | After | Savings |
|---|---|---|---|
| Shared Connection Pool | 5+ pool creations | 1 shared pool | 1-2s |
| Seeders vs HTTP | ~200ms per setup | ~20ms per setup | 10x faster |
| Shared Admin Sessions | 70+ bcrypt ops | 5 bcrypt ops | 10-14s |
| Transaction Isolation | 50-100ms per test | 5-10ms per test | 10-20x |
| Parallel Execution | Sequential | 4 workers | 30-50% faster |
- Enable performance monitoring (already enabled by default)
- Check the performance report for slow tests
- Use transaction isolation for unit tests
- Use seeders instead of HTTP for test setup
- Run tests in parallel:
npm run test:parallel
- Ensure MySQL is running:
docker-compose up -d - Initialize test database:
npm run test:db:init - Check
.env.testconfiguration - Run setup script:
npm run test:setup
- Check for shared state between tests
- Ensure each test is independent
- Use
beforeEachfor test setup, notbeforeAll - Check for race conditions in parallel execution
- Check if disabled:
DISABLE_PERF_MONITOR=true - Ensure tests are actually slow (>1s)
- Check console output at end of test suite
const { exportPerformanceData } = require('./helpers/performanceMonitor');
afterAll(() => {
exportPerformanceData('./reports/test-performance.json');
});For true parallel integration tests (advanced):
// Use JEST_WORKER_ID to create separate databases
const workerDB = `taxoffice_test_w${process.env.JEST_WORKER_ID}`;# .github/workflows/test.yml
- name: Run tests with coverage
run: npm run test:coverage
- name: Upload performance report
uses: actions/upload-artifact@v2
with:
name: test-performance
path: test-performance.jsonWhen adding new tests:
- Use test builders for data generation
- Use seeders for database setup
- Add custom matchers for domain logic
- Keep tests fast (<1s if possible)
- Ensure tests are independent
- Check performance report for regressions
Last Updated: 2025-12-03 Version: 2.0.0 (with Phase 1, 2, 3 optimizations)