Comprehensive testing documentation for the NT TaxOffice appointment booking system.
- Overview
- Test Structure
- Running Tests
- Test Infrastructure
- Writing Tests
- CI/CD Integration
- Troubleshooting
The project uses a comprehensive testing strategy with multiple layers:
- Unit Tests: Test individual functions and modules in isolation
- Integration Tests: Test API endpoints with database integration
- E2E Tests: Test complete user workflows using Playwright
- Jest: Unit and integration testing
- Playwright: End-to-end browser testing
- Supertest: HTTP endpoint testing
- Minimum: 70% coverage for branches, functions, lines, and statements
- Target: 80%+ coverage for critical business logic
tests/
├── setup.js # Frontend test setup (jsdom)
├── setup-backend.js # Backend test setup (node)
├── helpers/
│ ├── database.js # Database test utilities
│ ├── fixtures.js # Test data factories
│ ├── mocks.js # Mock implementations
│ └── testApp.js # Test Express app setup
├── unit/
│ ├── utils/
│ │ ├── validation.test.js # Validation utility tests
│ │ ├── sanitization.test.js # Sanitization utility tests
│ │ └── timezone.test.js # Timezone utility tests
│ ├── services/
│ │ └── appointments.test.js # Appointments service tests
│ └── middleware/
│ └── (middleware tests)
├── integration/
│ ├── api/
│ │ └── appointments.test.js # Public API integration tests
│ └── admin/
│ ├── auth.test.js # Admin auth tests (10 tests)
│ ├── appointments.test.js # Admin appointments tests (26 tests)
│ └── availability.test.js # Admin availability tests (22 tests)
├── frontend/
│ └── (frontend component tests)
└── e2e/
└── appointmentBooking.spec.js # E2E appointment booking test
-
Install dependencies:
npm install
-
Set up test environment:
cp .env.example .env.test
-
Edit
.env.testwith test database credentials:DB_HOST=localhost DB_USER=nt_taxoffice_test DB_PASSWORD=test_password DB_NAME=nt_taxoffice_appointments_test
-
Create test database:
mysql -u root -p CREATE DATABASE nt_taxoffice_appointments_test; CREATE USER 'nt_taxoffice_test'@'localhost' IDENTIFIED BY 'test_password'; GRANT ALL PRIVILEGES ON nt_taxoffice_appointments_test.* TO 'nt_taxoffice_test'@'localhost'; FLUSH PRIVILEGES;
Run all tests (sequential, stable for CI):
npm testFast parallel execution (development):
npm run test:parallelRun only unit tests (backend + frontend):
npm run test:unitRun only backend unit tests:
npm run test:backendRun only frontend unit tests:
npm run test:frontendRun integration tests:
npm run test:integrationFast unit tests in parallel:
npm run test:fastRun specific test suites:
# Admin API tests
npm run test:admin
# Public API tests
npm run test:api
# Service layer tests
npm run test:servicesRun admin integration tests only:
npm run test:integration -- tests/integration/admin/Run specific admin test file:
npm run test:integration -- tests/integration/admin/appointments.test.jsRun E2E tests:
npm run test:e2eRun all test types:
npm run test:allWatch mode (auto-rerun on changes):
npm run test:watchGenerate coverage report:
npm run test:coverageCoverage report will be in coverage/lcov-report/index.html.
Tests use a separate .env.test file to avoid affecting development/production:
- Test database (isolated from dev database)
- Lower bcrypt rounds (faster tests)
- Relaxed rate limiting
- Mock email sending
The project includes optimized test utilities for fast, maintainable tests:
Why: Eliminates redundant database connection overhead (100+ connections → 1 shared pool)
Located in /tests/helpers/testDatabase.js:
const { getTestDatabase } = require('./helpers/testDatabase');
beforeAll(async () => {
await getTestDatabase(); // Initializes shared pool once
});
// All tests share the same connection pool
// No need to create separate pools per fileWhy: Readable, chainable API for creating realistic test data with Greek locale support
Located in /tests/helpers/builders/:
AppointmentBuilder - Create test appointments:
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);AdminBuilder - Create admin users:
const { AdminBuilder } = require('./helpers/builders');
const admin = new AdminBuilder()
.withUsername('admin')
.withPassword('SecurePass123!')
.withGreekEmail()
.build();Why: Direct DB inserts bypass HTTP overhead (10x faster than API calls)
Located in /tests/helpers/seeders.js:
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 },
]);Why: Domain-specific assertions for clearer tests
Located in /tests/helpers/customMatchers.js:
// Automatically loaded in test setup
// 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();Why: 10-20x faster than truncating tables (5-10ms vs 50-100ms per test)
Located in /tests/helpers/transactionHelper.js:
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
});
});Why: Automatically identify slow tests and optimization opportunities
Located in /tests/helpers/performanceMonitor.js:
// Performance monitoring is enabled by default
// Disable with: DISABLE_PERF_MONITOR=true npm test
// Automatically tracks tests >1s and reports them
// No setup required - integrated into test:setup-backend.jsPerformance 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
These helpers are still available for backward compatibility:
Clear data between tests:
const { clearTestDatabase } = require('./helpers/database');
await clearTestDatabase();Run raw queries:
const { query } = require('./helpers/database');
const results = await query('SELECT * FROM appointments WHERE id = ?', [1]);Located in /tests/helpers/mocks.js:
Mock Express request/response:
const { createMockRequest, createMockResponse, createMockNext } = require('./helpers/mocks');
const req = createMockRequest({
body: { name: 'test' },
params: { id: 1 },
});
const res = createMockResponse();
const next = createMockNext();
await yourMiddleware(req, res, next);
expect(res.json).toHaveBeenCalledWith({ success: true });Mock database connection:
const { createMockDbPool } = require('./helpers/mocks');
const mockPool = createMockDbPool();
mockPool.query.mockResolvedValueOnce([[{ id: 1 }]]);Mock email transporter:
const { createMockEmailTransporter } = require('./helpers/mocks');
const transporter = createMockEmailTransporter();
expect(transporter.sendMail).toHaveBeenCalled();Recent optimizations have achieved 30-40% faster test execution:
| 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 |
// tests/unit/services/myService.test.js
const { createMockDbPool } = require('../../helpers/mocks');
const myService = require('../../../services/myService');
// Mock dependencies
jest.mock('../../../services/database');
const database = require('../../../services/database');
describe('My Service', () => {
let mockPool;
beforeEach(() => {
mockPool = createMockDbPool();
database.getDb.mockReturnValue(mockPool);
});
test('should process data correctly', async () => {
mockPool.query.mockResolvedValueOnce([[{ id: 1, name: 'Test' }]]);
const result = await myService.getData(1);
expect(result).toEqual({ id: 1, name: 'Test' });
expect(mockPool.query).toHaveBeenCalledWith(expect.stringContaining('SELECT'), [1]);
});
});// tests/integration/api/myRoute.test.js
const request = require('supertest');
const express = require('express');
const { clearTestDatabase } = require('../../helpers/database');
describe('My Route API', () => {
let app;
beforeAll(async () => {
app = express();
app.use(express.json());
app.use('/api/my-route', require('../../../routes/api/myRoute'));
});
beforeEach(async () => {
await clearTestDatabase();
});
test('POST /api/my-route should create resource', async () => {
const data = { name: 'Test Resource' };
const response = await request(app).post('/api/my-route').send(data).expect(201);
expect(response.body.success).toBe(true);
expect(response.body.resource.name).toBe('Test Resource');
});
});// tests/e2e/myFeature.spec.js
const { test, expect } = require('@playwright/test');
test.describe('My Feature', () => {
test('should complete user workflow', async ({ page }) => {
await page.goto('/my-feature');
await page.fill('#input-field', 'Test Value');
await page.click('#submit-button');
await expect(page.locator('#success-message')).toBeVisible();
await expect(page.locator('#success-message')).toContainText('Success');
});
});The project uses GitHub Actions for automated testing on every push and pull request. View the latest test results:
Workflow Configuration (.github/workflows/test.yml)
The CI pipeline:
-
Environment Setup
- Runs on Ubuntu latest
- Uses Node.js 18
- Starts MySQL 8.0 service container
-
Test Execution
- Initializes test database with schema
- Runs unit tests (
npm run test:unit) - Runs integration tests (
npm run test:integration) - Generates coverage report (
npm run test:coverage)
-
Artifacts
- Uploads coverage reports (retained for 30 days)
- Available in Actions tab for each run
Viewing CI Results:
- Check the Actions tab on GitHub
- Pull request checks show inline test results
- Click on workflow runs to view detailed logs
Local vs CI Environment:
The CI environment differs from local:
- Uses
DB_HOST=127.0.0.1(GitHub Actions MySQL service) - Tests run with
NODE_ENV=test - All environment variables are explicitly set
- Fresh database for each run (no state carryover)
Install Husky for pre-commit testing:
npm install --save-dev husky
npx husky install
npx husky add .husky/pre-commit "npm run test:unit"Error: Access denied for user 'nt_taxoffice_test'
Solution:
mysql -u root -p
GRANT ALL PRIVILEGES ON nt_taxoffice_appointments_test.* TO 'nt_taxoffice_test'@'localhost';
FLUSH PRIVILEGES;Issue: Tests don't complete, process hangs
Causes:
- Open database connections
- Timers not cleared
- Async operations not awaited
Solution:
- Ensure
afterAllhooks close connections - Use
jest.setTimeout()to increase timeout - Check for unresolved promises
Error: Port 3000 is already in use
Solution:
# Kill process using port 3000
lsof -ti:3000 | xargs kill -9
# Or use different port in .env.test
PORT=3001Symptoms: Tests pass sometimes, fail other times
Common Causes:
- Race conditions
- Timing issues
- Database state not cleared
- Shared state between tests
Solutions:
- Use
clearTestDatabase()inbeforeEach - Avoid shared variables between tests
- Use
waitForin async operations - Mock time-dependent functions
Issue: Module changes don't reflect in tests
Solution:
// Mock BEFORE importing the module
jest.mock('../../../services/database');
const myModule = require('../../../services/myModule');
// Clear mocks between tests
beforeEach(() => {
jest.clearAllMocks();
});Each test should be independent and not rely on other tests:
beforeEach(async () => {
await clearTestDatabase();
// Reset any shared state
});Use clear, descriptive test names:
// Good
test('should return 404 when appointment not found', ...)
// Bad
test('test 1', ...)Structure tests clearly:
test('should create appointment', async () => {
// Arrange
const data = createAppointmentData();
// Act
const result = await appointments.create(data);
// Assert
expect(result.id).toBeDefined();
expect(result.status).toBe('pending');
});Don't just test happy paths:
test('should throw error for invalid email', async () => {
const data = createAppointmentData({ client_email: 'invalid' });
await expect(appointments.create(data)).rejects.toThrow('Invalid email');
});Never make real API calls or send real emails in tests:
jest.mock('../../../services/email');
jest.mock('../../../services/externalApi');After running npm run test:coverage:
# Open in browser
open coverage/lcov-report/index.html
# Or use terminal
cat coverage/coverage-summary.jsonAdd to README.md:
The project includes comprehensive admin integration tests covering authentication, appointment management, and availability settings.
-
Admin Authentication (
tests/integration/admin/auth.test.js): 10 tests- Admin setup, login, logout, session management
-
Admin Appointments (
tests/integration/admin/appointments.test.js): 26 tests- GET /api/admin/appointments (filtering, pagination, sorting)
- GET /api/admin/appointments/stats (statistics)
- GET /api/admin/appointments/:id (details with history)
- PUT /api/admin/appointments/:id/status (confirm, decline, complete)
- PUT /api/admin/appointments/:id (update details)
- DELETE /api/admin/appointments/:id (deletion)
-
Admin Availability (
tests/integration/admin/availability.test.js): 22 tests- GET /api/admin/availability/settings (retrieve settings)
- PUT /api/admin/availability/settings (update with validation)
- GET /api/admin/availability/blocked-dates (future dates)
- POST /api/admin/availability/blocked-dates (add blocked date)
- DELETE /api/admin/availability/blocked-dates/:id (remove blocked date)
# All admin tests
npm run test:integration -- tests/integration/admin/
# Specific test file
npm run test:integration -- tests/integration/admin/appointments.test.js
# Single test by name
npm run test:integration -- tests/integration/admin/appointments.test.js -t "should get appointments with pagination"Admin tests use session-based authentication:
// Create admin and login
await request(app).post('/api/admin/setup').send({
username: 'admin',
email: 'admin@example.com',
password: 'SecurePass123!',
confirmPassword: 'SecurePass123!',
});
agent = request.agent(app);
await agent.post('/api/admin/login').send({
username: 'admin',
password: 'SecurePass123!',
});
// Now agent maintains session cookies
await agent.get('/api/admin/appointments').expect(200);Learn More: See Admin Testing Guide for comprehensive admin testing documentation.
- Comprehensive Testing Guide - Complete guide with best practices, performance tips, and all test utilities
- Admin Testing Guide - Detailed guide for admin integration tests
- Jest Documentation
- Playwright Documentation
- Supertest Documentation
- Testing Best Practices
If you encounter issues with testing:
- Check this documentation
- Review existing test files for examples
- Check test output for detailed error messages
- Ensure test database is properly configured
- Verify all dependencies are installed
Last Updated: December 3, 2025