Testing Requirements
Test Organization
/backend/tests/
├── unit/ # Unit tests for isolated logic
│ ├── models/ # Model tests
│ │ ├── task.test.js
│ │ ├── project.test.js
│ │ ├── user.test.js
│ │ └── ...
│ ├── middleware/ # Middleware tests
│ │ ├── auth.test.js
│ │ └── authorize.test.js
│ ├── services/ # Service tests
│ │ ├── permissionsService.test.js
│ │ ├── applyPerms.test.js
│ │ └── ...
│ └── utils/ # Utility tests
│ ├── timezone-utils.test.js
│ ├── slug-utils.test.js
│ ├── attachment-utils.test.js
│ └── migration-utils.test.js
│
└── integration/ # Integration tests for API endpoints
├── tasks/
│ ├── tasks.test.js
│ ├── subtasks.test.js
│ └── recurring.test.js
├── projects/
│ └── projects.test.js
├── areas/
├── notes/
├── tags/
├── auth/
├── shares/
└── ... (47+ test directories)
/e2e/tests/ # E2E tests (Playwright)
├── login.spec.ts
├── tasks.spec.ts
├── projects.spec.ts
├── subtasks.spec.ts
└── ...
/frontend/__tests__/ # Frontend tests
├── setup.ts # Test configuration
└── components/
└── ... (component tests)
Running Tests
Backend Tests
# Run all backend tests
npm test
# or
npm run backend:test
# Run specific test file
npm test -- backend/tests/unit/models/task.test.js
# Run with coverage
npm run test:coverage
# Watch mode (re-run on file changes)
npm run test:watch
Frontend Tests
# Run frontend tests
npm run frontend:test
# Watch mode
npm run frontend:test -- --watch
E2E Tests
# Headless mode (default)
npm run test:ui
# Headed mode (see browser)
npm run test:ui:headed
# Specific test file
npx playwright test e2e/tests/tasks.spec.ts
# Debug mode
npx playwright test --debug
Pre-Push Checks
# Run all checks before committing/pushing
npm run pre-push
# This runs:
# - ESLint checks
# - Prettier formatting
# - Backend tests
# - Type checking (if applicable)
Testing Requirements
For Bug Fixes
MUST include a test that would have caught the bug.
Process:
- Write failing test that demonstrates the bug
- Fix the bug
- Verify test now passes
- Submit PR with both test and fix
Example:
// Test for bug: completed tasks showing in Today view
it('should not return completed tasks in Today view', async () => {
// Arrange - Create completed task
await Task.create({
name: 'Completed Task',
status: 2, // completed
due_date: new Date().toISOString().split('T')[0],
user_id: user.id
});
// Act - Get today's tasks
const response = await request(app)
.get('/api/v1/tasks/today')
.set('Cookie', authCookie);
// Assert - No completed tasks
expect(response.status).toBe(200);
const completedTasks = response.body.filter(t => t.status === 2);
expect(completedTasks.length).toBe(0);
});
For New Features
SHOULD include relevant tests covering:
- Happy path (success case)
- Common edge cases
- Error conditions
Not required to test:
- Every possible combination
- Framework internals
- Third-party library behavior
Test Patterns
Backend Integration Test
Arrange-Act-Assert Pattern:
// /backend/tests/integration/tasks/tasks.test.js
const request = require('supertest');
const app = require('../../../app');
const { Task, User } = require('../../../models');
describe('Task API', () => {
let user;
let authCookie;
beforeEach(async () => {
// Setup: Create user and authenticate
user = await User.create({
email: 'test@example.com',
password: 'password123'
});
const res = await request(app)
.post('/api/login')
.send({ email: 'test@example.com', password: 'password123' });
authCookie = res.headers['set-cookie'];
});
afterEach(async () => {
// Cleanup
await Task.destroy({ where: {} });
await User.destroy({ where: {} });
});
it('should create task with valid data', async () => {
// Arrange
const taskData = {
name: 'Test Task',
priority: 1,
due_date: '2026-03-15'
};
// Act
const response = await request(app)
.post('/api/v1/task')
.set('Cookie', authCookie)
.send(taskData);
// Assert
expect(response.status).toBe(201);
expect(response.body.name).toBe('Test Task');
expect(response.body.priority).toBe(1);
// Verify in database
const task = await Task.findOne({ where: { name: 'Test Task' } });
expect(task).not.toBeNull();
expect(task.user_id).toBe(user.id);
});
it('should return 400 for missing name', async () => {
// Arrange
const invalidData = { priority: 1 };
// Act
const response = await request(app)
.post('/api/v1/task')
.set('Cookie', authCookie)
.send(invalidData);
// Assert
expect(response.status).toBe(400);
expect(response.body.error).toBeDefined();
});
it('should return 404 for non-existent task', async () => {
// Act
const response = await request(app)
.get('/api/v1/task/99999')
.set('Cookie', authCookie);
// Assert
expect(response.status).toBe(404);
});
});
Backend Unit Test
// /backend/tests/unit/utils/timezone-utils.test.js
const { getTodayBoundsInUTC } = require('../../../utils/timezone-utils');
describe('timezone-utils', () => {
describe('getTodayBoundsInUTC', () => {
it('should return UTC bounds for today in given timezone', () => {
// Arrange
const timezone = 'America/New_York';
// Act
const { startOfDay, endOfDay } = getTodayBoundsInUTC(timezone);
// Assert
expect(startOfDay).toBeInstanceOf(Date);
expect(endOfDay).toBeInstanceOf(Date);
expect(endOfDay.getTime()).toBeGreaterThan(startOfDay.getTime());
});
it('should handle invalid timezone gracefully', () => {
// Arrange
const invalidTimezone = 'Invalid/Timezone';
// Act & Assert
expect(() => getTodayBoundsInUTC(invalidTimezone)).not.toThrow();
});
});
});
Frontend Component Test
// /frontend/components/Task/__tests__/TaskItem.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { TaskItem } from '../TaskItem';
import { Task } from '../../../entities/Task';
describe('TaskItem', () => {
const mockTask: Task = {
id: 1,
uid: 'test-uid-123',
name: 'Test Task',
completed: false,
priority: 1,
due_date: '2026-03-15'
};
it('renders task name', () => {
// Act
render(<TaskItem task={mockTask} onUpdate={jest.fn()} />);
// Assert
expect(screen.getByText('Test Task')).toBeInTheDocument();
});
it('shows priority badge', () => {
// Act
render(<TaskItem task={mockTask} onUpdate={jest.fn()} />);
// Assert
expect(screen.getByText('Medium')).toBeInTheDocument();
});
it('calls onUpdate when checkbox is clicked', () => {
// Arrange
const mockOnUpdate = jest.fn();
render(<TaskItem task={mockTask} onUpdate={mockOnUpdate} />);
// Act
const checkbox = screen.getByRole('checkbox');
fireEvent.click(checkbox);
// Assert
expect(mockOnUpdate).toHaveBeenCalledWith({
...mockTask,
completed: true
});
});
it('applies completed styling when task is done', () => {
// Arrange
const completedTask = { ...mockTask, completed: true };
// Act
render(<TaskItem task={completedTask} onUpdate={jest.fn()} />);
// Assert
const taskElement = screen.getByText('Test Task').closest('div');
expect(taskElement).toHaveClass('line-through');
expect(taskElement).toHaveClass('opacity-50');
});
});
E2E Test (Playwright)
// /e2e/tests/tasks.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Task Management', () => {
test.beforeEach(async ({ page }) => {
// Login before each test
await page.goto('http://localhost:8080/login');
await page.fill('input[name="email"]', 'test@example.com');
await page.fill('input[name="password"]', 'password123');
await page.click('button[type="submit"]');
await page.waitForURL('**/tasks');
});
test('should create new task', async ({ page }) => {
// Arrange
await page.click('button:has-text("New Task")');
// Act
await page.fill('input[name="name"]', 'E2E Test Task');
await page.selectOption('select[name="priority"]', '1');
await page.fill('input[name="due_date"]', '2026-03-15');
await page.click('button:has-text("Save")');
// Assert
await expect(page.locator('text=E2E Test Task')).toBeVisible();
});
test('should complete task', async ({ page }) => {
// Arrange - Create a task first
await page.click('button:has-text("New Task")');
await page.fill('input[name="name"]', 'Task to Complete');
await page.click('button:has-text("Save")');
// Act - Complete the task
const taskItem = page.locator('text=Task to Complete').locator('..');
await taskItem.locator('input[type="checkbox"]').check();
// Assert
await expect(taskItem).toHaveClass(/line-through/);
});
test('should filter tasks by priority', async ({ page }) => {
// Arrange - Create tasks with different priorities
await createTask(page, 'High Priority Task', 2);
await createTask(page, 'Low Priority Task', 0);
// Act - Filter by high priority
await page.selectOption('select[name="priority_filter"]', '2');
// Assert
await expect(page.locator('text=High Priority Task')).toBeVisible();
await expect(page.locator('text=Low Priority Task')).not.toBeVisible();
});
});
async function createTask(page, name: string, priority: number) {
await page.click('button:has-text("New Task")');
await page.fill('input[name="name"]', name);
await page.selectOption('select[name="priority"]', priority.toString());
await page.click('button:has-text("Save")');
await page.waitForSelector(`text=${name}`);
}
Test Database
Backend tests use a separate test database:
- Automatically created in test environment
- Migrations run before tests
- Database cleared between tests (in
afterEach) - Configured in
/backend/config/database.js
Example cleanup:
afterEach(async () => {
// Clean up test data
await Task.destroy({ where: {} });
await Project.destroy({ where: {} });
await User.destroy({ where: {} });
});
Mocking
Mock External Services
// Mock email service in tests
jest.mock('../../../services/emailService', () => ({
sendEmail: jest.fn().mockResolvedValue(true)
}));
it('should send notification email', async () => {
const emailService = require('../../../services/emailService');
await taskService.create({ name: 'Task', notify: true }, userId);
expect(emailService.sendEmail).toHaveBeenCalled();
});
Mock Frontend API Calls
import { rest } from 'msw';
import { setupServer } from 'msw/node';
const server = setupServer(
rest.get('/api/v1/tasks', (req, res, ctx) => {
return res(ctx.json([
{ id: 1, name: 'Mocked Task' }
]));
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
Coverage Goals
While not strictly enforced, aim for:
- Critical paths: 80%+ coverage
- Business logic: 70%+ coverage
- UI components: 50%+ coverage
Run coverage report:
npm run test:coverage
# Open HTML report
open coverage/index.html
Before Submitting PR
✅ All tests passing:
npm test
npm run test:ui
✅ No linting errors:
npm run lint
✅ Code formatted:
npm run format:fix
✅ Run pre-push checks:
npm run pre-push