- Overview
- Testing Strategy
- Backend Testing
- Frontend Testing
- Test Setup & Configuration
- Writing Tests
- Best Practices
- Troubleshooting
- Test Coverage
The Property Management System (PMS) uses a comprehensive multi-layered testing approach to ensure reliability, maintainability, and quality across both frontend and backend codebases.
/\
/E2E\ ← End-to-End Tests (Few, Critical Paths)
/------\
/Integration\ ← Integration Tests (Some, Key Interactions)
/------------\
/ Unit Tests \ ← Unit Tests (Many, Individual Components)
/----------------\
- Backend: ✅ All E2E tests passing
- Frontend: ✅ All 60 unit tests passing
- Coverage: Meeting threshold requirements
- Purpose: Test individual functions, methods, and components in isolation
- Backend: Service classes, utilities, helpers
- Frontend: React components, hooks, utility functions
- Speed: Fast (< 100ms per test)
- Isolation: Fully mocked dependencies
- Purpose: Test interactions between multiple components/services
- Backend: Controller + Service + Database interactions
- Frontend: Component interactions, API client usage
- Speed: Medium (100ms - 1s per test)
- Isolation: Partial mocking (e.g., external APIs)
- Purpose: Test complete user workflows from start to finish
- Backend: Full API request/response cycles with database
- Frontend: Browser-based user interactions (Playwright)
- Speed: Slow (1s - 10s per test)
- Isolation: Real database, mocked external services
- Test Independence: Each test can run in isolation
- Deterministic: Tests produce consistent results
- Fast Feedback: Quick execution for rapid iteration
- Maintainable: Easy to update when code changes
- Readable: Clear test names and structure
- Comprehensive: Cover happy paths, edge cases, and error scenarios
- Framework: Jest
- E2E Testing: Supertest
- Database: PostgreSQL (test database)
- ORM: Prisma
cd tenant_portal_backend
npm install
# Ensure test database is running
# Default: postgresql://postgres:jordan@localhost:5432/tenant_portal_test# Run all unit tests
npm test
# Run all tests in watch mode
npm run test:watch
# Run with coverage report
npm run test:coverage
# Run only E2E tests (serial execution)
npm run test:e2e
# Run specific test file
npm run test:e2e -- payments.e2e.spec.ts
# Run specific test by name
npm run test:e2e -- -t "should create payment"tenant_portal_backend/
├── src/
│ └── **/*.spec.ts # Unit tests (co-located with source)
├── test/
│ ├── setup.ts # Global test setup
│ ├── jest-e2e.json # E2E test configuration
│ ├── factories/
│ │ └── index.ts # Test data factories
│ ├── utils/
│ │ └── reset-database.ts # Database reset utility
│ └── **/*.e2e.spec.ts # E2E test files
| File | Description |
|---|---|
auth.e2e.spec.ts |
Authentication flows (login, register, JWT) |
payments.e2e.spec.ts |
Payment processing and invoice management |
leasing.e2e.spec.ts |
Leasing workflows and lead management |
esignature.e2e.spec.ts |
E-signature envelope creation and management |
application-lifecycle.e2e.spec.ts |
Rental application lifecycle |
E2E tests use a separate PostgreSQL database configured in test/setup.ts:
const TEST_DB_URL = process.env.DATABASE_URL ||
'postgresql://postgres:jordan@localhost:5432/tenant_portal_test?schema=public_';Tests use a robust database reset mechanism:
// test/utils/reset-database.ts
export async function resetDatabase(prisma: PrismaLike): Promise<void> {
// Truncates all tables with RESTART IDENTITY CASCADE
// Ensures clean state for each test
}Key Features:
- Truncates all public tables (except migrations)
- Resets auto-increment sequences
- Handles foreign key constraints with CASCADE
- Retry logic for deadlock prevention
Use TestDataFactory for consistent test data:
import { TestDataFactory } from '../factories';
const user = await prisma.user.create({
data: TestDataFactory.createUser({
username: 'test@example.com',
role: 'TENANT',
}),
});
const lease = await prisma.lease.create({
data: TestDataFactory.createLease(user.id, unit.id, {
rentAmount: 2000,
status: LeaseStatus.ACTIVE,
}),
});import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
import { PrismaService } from '../src/prisma/prisma.service';
import { TestDataFactory } from './factories';
import { resetDatabase } from './utils/reset-database';
describe('Payments API (e2e)', () => {
let app: INestApplication;
let prisma: PrismaService;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
prisma = app.get<PrismaService>(PrismaService);
});
beforeEach(async () => {
await resetDatabase(prisma);
});
it('should create payment for invoice', async () => {
// Arrange
const tenantUser = await prisma.user.create({
data: TestDataFactory.createUser({ role: 'TENANT' }),
});
const unit = await prisma.unit.create({
data: TestDataFactory.createUnit(property.id),
});
const lease = await prisma.lease.create({
data: TestDataFactory.createLease(tenantUser.id, unit.id),
});
const invoice = await prisma.invoice.create({
data: TestDataFactory.createInvoice(lease.id),
});
// Act
const response = await request(app.getHttpServer())
.post('/api/payments')
.set('Authorization', `Bearer ${tenantToken}`)
.send({
invoiceId: invoice.id,
leaseId: lease.id,
amount: 2000,
paymentMethodId: null,
});
// Assert
expect(response.status).toBe(201);
expect(response.body).toHaveProperty('id');
expect(response.body.amount).toBe(2000);
});
afterAll(async () => {
await app.close();
await prisma.$disconnect();
});
});- Unit tests configuration
- Excludes E2E tests
- Includes
test/setup.tsfor global setup
- Separate configuration for E2E tests
- Uses
--runInBandflag for serial execution (prevents database deadlocks) - Includes
test/setup.tsfor database setup
- Framework: Vitest
- Testing Library: React Testing Library
- E2E Testing: Playwright
- API Mocking: MSW (Mock Service Worker)
- Environment: jsdom
cd tenant_portal_app
npm install
# Initialize MSW (if not already done)
npm run msw:init
# Install Playwright browsers (first time only)
npx playwright install# Unit tests - watch mode
npm test
# Unit tests - run once
npm run test:run
# Unit tests - with coverage
npm run test:coverage
# Unit tests - visual UI
npm run test:ui
# E2E tests - all browsers
npm run test:e2e
# E2E tests - UI mode (interactive)
npm run test:e2e:ui
# E2E tests - with browser visible
npm run test:e2e:headed
# E2E tests - debug mode
npm run test:e2e:debug
# Run all tests (unit + E2E)
npm run test:alltenant_portal_app/
├── src/
│ ├── **/*.test.tsx # Unit tests (co-located)
│ ├── **/*.test.ts # Unit tests for services
│ ├── test/
│ │ └── setup.ts # Global test setup (MSW, mocks)
│ └── mocks/
│ ├── server.ts # MSW server setup
│ └── handlers.ts # API request handlers
└── e2e/
└── **/*.spec.ts # Playwright E2E tests
MSW intercepts HTTP requests in tests, allowing you to mock API responses without a real backend.
// src/test/setup.ts
import { server } from '../mocks/server';
beforeAll(() => {
server.listen({ onUnhandledRequest: 'error' });
});
afterEach(() => {
server.resetHandlers();
});
afterAll(() => {
server.close();
});// src/mocks/handlers.ts
import { http, HttpResponse } from 'msw';
export const handlers = [
http.post(`${API_BASE}/api/endpoint`, async ({ request }) => {
const body = await request.json();
return HttpResponse.json({
id: 1,
...body,
createdAt: new Date().toISOString(),
}, { status: 201 });
}),
];- ✅ Authentication (
/api/auth/*) - ✅ Properties (
/api/properties/*) - ✅ Leases (
/api/lease/*) - ✅ Payments (
/api/payments/*) - ✅ Maintenance (
/api/maintenance/*) - ✅ Messaging (
/api/messaging/*) - ✅ Leads (
/api/leads/*) - ✅ Property Search (
/api/properties/search) - ✅ Bulk Messaging (
/api/messaging/bulk/*)
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { BrowserRouter } from 'react-router-dom';
import BulkMessageComposer from './BulkMessageComposer';
import * as apiClient from '../../services/apiClient';
// Mock apiFetch
const mockApiFetch = vi.fn();
beforeEach(() => {
vi.spyOn(apiClient, 'apiFetch').mockImplementation(mockApiFetch);
});
describe('BulkMessageComposer', () => {
it('submits preview payload with selected filters', async () => {
mockApiFetch.mockResolvedValue({
totalRecipients: 2,
sample: [],
});
render(
<BrowserRouter>
<BulkMessageComposer
token="token"
templates={[{ id: 1, name: 'Reminder', body: 'Hello {{username}}' }]}
/>
</BrowserRouter>
);
await userEvent.selectOptions(screen.getByLabelText(/Template/i), ['1']);
await userEvent.type(screen.getByLabelText(/Subject/i), 'Rent notice');
await userEvent.type(screen.getByLabelText(/Property IDs/i), '5,6');
await userEvent.click(screen.getByLabelText(/Property managers/i));
await userEvent.click(screen.getByRole('button', { name: /Preview recipients/i }));
await waitFor(() => {
expect(mockApiFetch).toHaveBeenCalled();
});
const [endpoint, options] = mockApiFetch.mock.calls[0];
expect(endpoint).toBe('/messaging/bulk/preview');
expect(options.body.filters.propertyIds).toEqual([5, 6]);
await waitFor(() => {
expect(screen.getByText(/will reach/i)).toBeInTheDocument();
});
});
});import { AuthProvider } from '../../AuthContext';
// Mock useAuth
vi.mock('../../AuthContext', () => ({
useAuth: () => ({
token: 'test-token',
user: { id: 1, role: 'TENANT' },
login: vi.fn(),
logout: vi.fn(),
}),
}));
render(
<BrowserRouter>
<AuthProvider>
<Component />
</AuthProvider>
</BrowserRouter>
);import { BrowserRouter } from 'react-router-dom';
// Mock useNavigate
const mockNavigate = vi.fn();
vi.mock('react-router-dom', async () => {
const actual = await vi.importActual('react-router-dom');
return {
...actual,
useNavigate: () => mockNavigate,
};
});Located in e2e/ directory:
authentication.spec.ts- Login, logout, registrationdashboard.spec.ts- Dashboard viewsmaintenance.spec.ts- Maintenance request workflowspayments.spec.ts- Payment processinglease-management.spec.ts- Lease operationsmessaging.spec.ts- Messaging flowsapplication-submission.spec.ts- Application workflows
// Global test configuration
process.env.DATABASE_URL = TEST_DB_URL;
process.env.JWT_SECRET = 'test-secret-key';
process.env.MONITORING_ENABLED = 'false';
process.env.DISABLE_WORKFLOW_SCHEDULER = 'true';
// Apply migrations
execSync('npx prisma migrate deploy', { stdio: 'inherit' });
// Database reset (only for E2E tests)
if (process.env.E2E_TEST_RUNNER === 'true') {
beforeEach(async () => {
await resetDatabase(prismaTestClient);
});
}// MSW server setup
import { server } from '../mocks/server';
beforeAll(() => {
server.listen({ onUnhandledRequest: 'error' });
});
afterEach(() => {
cleanup();
vi.clearAllMocks();
server.resetHandlers();
});
afterAll(() => {
server.close();
});
// Mock window APIs
Object.defineProperty(window, 'matchMedia', { ... });
global.IntersectionObserver = class IntersectionObserver { ... };DATABASE_URL=postgresql://postgres:password@localhost:5432/tenant_portal_test
JWT_SECRET=test-secret-key
MONITORING_ENABLED=false
DISABLE_WORKFLOW_SCHEDULER=true
E2E_TEST_RUNNER=true # Enables database resetVITE_API_URL=http://localhost:3001/api # Optional, defaults to /apiit('should perform action', async () => {
// Arrange - Set up test data and mocks
const input = { value: 'test' };
mockFunction.mockResolvedValue({ success: true });
// Act - Execute the code being tested
const result = await functionUnderTest(input);
// Assert - Verify the results
expect(result).toEqual({ success: true });
expect(mockFunction).toHaveBeenCalledWith(input);
});- Test files:
*.test.ts,*.test.tsx,*.spec.ts,*.e2e.spec.ts - Test suites:
describe('ComponentName', () => { ... }) - Test cases:
it('should do something specific', () => { ... })
// Use waitFor for async UI updates
await waitFor(() => {
expect(screen.getByText('Loaded')).toBeInTheDocument();
}, { timeout: 3000 });
// Use findBy queries (auto-waits)
const element = await screen.findByText('Loaded');import userEvent from '@testing-library/user-event';
const user = userEvent.setup();
await user.click(screen.getByRole('button'));
await user.type(screen.getByLabelText('Email'), 'test@example.com');it('should handle API errors gracefully', async () => {
mockApiFetch.mockRejectedValue(new Error('Network error'));
render(<Component />);
await waitFor(() => {
expect(screen.getByText(/error/i)).toBeInTheDocument();
});
});it('should submit form with valid data', async () => {
const onSubmit = vi.fn();
render(<Form onSubmit={onSubmit} />);
await userEvent.type(screen.getByLabelText('Name'), 'John');
await userEvent.click(screen.getByRole('button', { name: /submit/i }));
await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith({ name: 'John' });
});
});✅ Do: Each test should be able to run in isolation
beforeEach(() => {
// Clean state before each test
vi.clearAllMocks();
resetDatabase();
});❌ Don't: Rely on test execution order
// BAD - Test depends on previous test
it('test 1', () => { global.state = 'value'; });
it('test 2', () => { expect(global.state).toBe('value'); });✅ Do: Describe what the test verifies
it('should create payment when invoice and lease are provided', () => { ... });
it('should return 400 when payment amount exceeds invoice balance', () => { ... });❌ Don't: Use vague or generic names
it('test payment', () => { ... });
it('works', () => { ... });✅ Do: Clearly separate setup, execution, and verification
it('should calculate total correctly', () => {
// Arrange
const items = [{ price: 10 }, { price: 20 }];
// Act
const total = calculateTotal(items);
// Assert
expect(total).toBe(30);
});✅ Do: Each test should verify one specific behavior
it('should validate email format', () => { ... });
it('should validate email is required', () => { ... });❌ Don't: Test multiple behaviors in one test
it('should validate email', () => {
// Testing format, required, and uniqueness all at once
});✅ Do: Use specific, meaningful matchers
expect(response.status).toBe(201);
expect(user.role).toBe('TENANT');
expect(array).toHaveLength(3);
expect(object).toHaveProperty('id');✅ Do: Mock external services and APIs
vi.mock('../../services/apiClient');
vi.spyOn(apiClient, 'apiFetch').mockResolvedValue(mockData);✅ Do: Clean up after tests
afterAll(async () => {
await app.close();
await prisma.$disconnect();
});✅ Do: Use proper waits and timeouts
await waitFor(() => {
expect(element).toBeInTheDocument();
}, { timeout: 3000 });❌ Don't: Use arbitrary delays
await new Promise(resolve => setTimeout(resolve, 1000)); // BADProblem: Connection refused or database does not exist
Solution:
# Ensure PostgreSQL is running
# Create test database if needed
createdb tenant_portal_test
# Verify connection string in test/setup.tsProblem: Foreign key constraint violated
Solution: Ensure proper cleanup order in beforeEach:
beforeEach(async () => {
// Delete in reverse dependency order
await prisma.payment.deleteMany();
await prisma.invoice.deleteMany();
await prisma.lease.deleteMany();
await prisma.user.deleteMany();
// Or use resetDatabase utility
await resetDatabase(prisma);
});Problem: deadlock detected errors
Solution:
- Use
--runInBandflag (already configured intest:e2escript) - Ensure
resetDatabasehas retry logic - Check for concurrent test execution
Problem: EADDRINUSE: address already in use
Solution:
# Find and kill process using port 3001
lsof -ti:3001 | xargs kill -9
# Or change test port in test configurationProblem: Endpoint not mocked errors
Solution:
- Verify handler is added to
src/mocks/handlers.ts - Check handler path matches request URL exactly
- Ensure MSW server is started in
beforeAll
Problem: Unable to find element
Solution:
// Use findBy queries (auto-waits)
const element = await screen.findByText('Text');
// Or use waitFor
await waitFor(() => {
expect(screen.getByText('Text')).toBeInTheDocument();
});Problem: Mock function not being called
Solution:
// Ensure mock is set up before component renders
beforeEach(() => {
vi.spyOn(module, 'function').mockImplementation(() => { ... });
});
// Or use vi.mock at top level
vi.mock('./module', () => ({
function: vi.fn(),
}));Problem: useAuth must be used within an AuthProvider
Solution:
// Mock useAuth before imports
vi.mock('../../AuthContext', () => ({
useAuth: () => ({
token: 'test-token',
user: { id: 1, role: 'TENANT' },
}),
}));| Error | Cause | Solution |
|---|---|---|
ReferenceError: jest is not defined |
Using Jest syntax in Vitest | Replace jest with vi |
Cannot read properties of undefined |
Missing mock or setup | Add proper mocks in beforeEach |
Timeout - Async callback was not invoked |
Test didn't complete | Add proper await or increase timeout |
Multiple elements found |
Query matches multiple elements | Use getAllBy* or more specific query |
- Lines: 80%+
- Functions: 80%+
- Branches: 75%+
- Statements: 80%+
- Lines: 60%+
- Functions: 60%+
- Branches: 60%+
- Statements: 60%+
cd tenant_portal_backend
npm run test:coverage
# View HTML report
open coverage/lcov-report/index.htmlcd tenant_portal_app
npm run test:coverage
# View HTML report
open coverage/index.htmlBoth configurations exclude:
- Test files themselves
- Configuration files
- Type definitions
- Mock data
- Node modules
Tests run automatically on:
- Push to
mainordevelopbranches - Pull requests
- Scheduled nightly runs
# Backend
cd tenant_portal_backend
CI=true npm run test:e2e
# Frontend
cd tenant_portal_app
CI=true npm run test:all- ✅ When adding new features
- ✅ When fixing bugs (add regression test)
- ✅ When refactoring code
- ✅ When changing API contracts
- ✅ When updating dependencies
- Test name clearly describes what it tests
- Test is independent and can run in isolation
- All assertions are meaningful
- Error cases are covered
- Edge cases are considered
- Test data is realistic
- Mocks are properly configured
- Cleanup is performed
- Jest Documentation
- Vitest Documentation
- React Testing Library
- Playwright Documentation
- MSW Documentation
test/factories/index.ts- Test data factory patternstest/utils/reset-database.ts- Database reset implementationsrc/mocks/handlers.ts- MSW handler examples
npm test # Unit tests
npm run test:watch # Watch mode
npm run test:coverage # Coverage report
npm run test:e2e # E2E testsnpm test # Unit tests (watch)
npm run test:run # Unit tests (once)
npm run test:coverage # Coverage report
npm run test:ui # Visual test UI
npm run test:e2e # E2E tests
npm run test:all # All tests// Mock API call
vi.spyOn(apiClient, 'apiFetch').mockResolvedValue(data);
// Wait for element
await waitFor(() => expect(element).toBeInTheDocument());
// User interaction
await userEvent.click(button);
await userEvent.type(input, 'text');
// Reset database
await resetDatabase(prisma);- Add visual regression testing
- Add performance testing
- Add accessibility testing (a11y)
- Increase coverage thresholds
- Add mutation testing
- Add contract testing
- Set up test reporting dashboard
Last Updated: January 2025 Maintained By: Development Team