Test Plan & Coverage - bounswe/bounswe2026group4 GitHub Wiki
- 1. Overview
- 2. Testing Strategy
- 3. Testing Frameworks & Tools
- 4. Test Architecture
- 5. Backend Test Plan
- 6. Frontend Test Plan
- 7. CI/CD Integration
- 8. Core Use Case Scenarios
- 9. User Acceptance Criteria
- 10. Traceability Matrix
This document defines the testing strategy, test plan, and acceptance criteria for the Local History Story Map project. The project follows a monorepo structure with a Django REST Framework backend, a React (Vite) frontend, and a React Native mobile app.
Testing Philosophy:
- Every feature must include unit tests (enforced via PR checklist)
- Tests must pass locally before opening a pull request
- CI pipeline gates merges on test results
We follow the standard test pyramid approach, prioritizing a large base of fast unit tests, supported by integration and API tests, with end-to-end tests covering critical user flows.
| Level | Scope | What We Test | Tools |
|---|---|---|---|
| Unit Tests | Individual functions, models, serializers, validators, components | Business logic correctness in isolation | pytest (backend), Vitest (frontend) |
| Integration / API Tests | API endpoints, service layer, multi-model interactions | Data flow between layers, HTTP contracts, error responses | pytest + DRF APIClient (backend), Vitest + mocked services (frontend) |
| E2E Tests | Full user flows across frontend and backend | Critical user journeys work end-to-end | Playwright (planned) |
-
Isolation: Each test is independent. State is reset between tests using
setup_method(backend) andbeforeEach(frontend). -
Real Database: Backend tests run against a real MySQL 8.0 instance (via
pytest-django), not mocks. This catches migration and constraint issues early. -
Fast Test Execution: Test settings use
MD5PasswordHasherinstead of production-grade hashers to keep the suite fast, andlocmem.EmailBackendfor in-memory email. -
DOM Simulation: Frontend tests run in a
jsdomenvironment, simulating browser behavior without a real browser. - User-Centric Frontend Tests: Frontend tests interact with components the way a real user would — via labels, roles, and visible text — following Testing Library best practices.
- Mock External Dependencies: API service calls in the frontend are mocked at the module level so component tests remain isolated from the backend.
| Backend Layer | What to Test |
|---|---|
| Models | Object creation, defaults, constraints, relationships, signals, validation, ordering |
| Serializers | Field validation, required/optional fields, duplicate detection, write-only behavior |
| Services | Business logic orchestration, error handling, edge cases |
| Views/API | HTTP status codes, response payloads, authentication/authorization, error formats |
| Permissions | Role-based access (guest, registered user, admin, owner) |
| Utilities | Custom validators, exception handlers, shared helpers |
| Frontend Layer | What to Test |
|---|---|
| Pages | Rendering, form validation, API submission, loading/error states, navigation |
| Components | Prop rendering, user interactions, accessibility attributes |
| Services | API call correctness (tested indirectly through page mocks or directly) |
| Tool | Purpose |
|---|---|
pytest + pytest-django
|
Test runner with Django integration (DB access, settings, fixtures) |
factory-boy |
Model factory library for consistent test data generation |
coverage |
Code coverage measurement and reporting |
unittest.mock |
Mocking for isolated unit tests that don't need the database |
Configuration (backend/pytest.ini):
[pytest]
addopts = --ds=config.settings.test
python_files = tests/test_*.py
python_classes = Test*
python_functions = test_*Test settings (backend/config/settings/test.py):
-
DEBUG = False— mirrors production behavior - Strips
debug_toolbarfrom apps and middleware - Uses in-memory email backend
- Uses fast password hasher for speed
| Tool | Purpose |
|---|---|
vitest |
Test runner integrated with Vite build system |
@testing-library/react |
Component rendering and DOM querying |
@testing-library/user-event |
Realistic user interaction simulation |
@testing-library/jest-dom |
Custom DOM assertion matchers (e.g., toBeInTheDocument()) |
jsdom |
Browser DOM environment for Node.js |
Configuration (in frontend/vite.config.js):
test: {
globals: true,
environment: "jsdom",
setupFiles: "./src/test/setup.js",
css: true,
}| Action | Command |
|---|---|
| Run backend tests | cd backend && pytest -v |
| Run frontend tests (watch mode) | cd frontend && npm test |
| Run frontend tests (single run, CI) | cd frontend && npm run test:run |
| Run frontend lint | cd frontend && npm run lint |
Each Django app follows a consistent test structure:
backend/
├── conftest.py # Shared fixtures (user, story, etc.)
├── <app>/
│ └── tests/
│ ├── test_models.py # Model layer tests
│ ├── test_serializers.py # Serializer validation tests
│ ├── test_services.py # Business logic / service layer tests
│ ├── test_views.py # API endpoint / view tests
│ └── test_permissions.py # Permission class tests
└── common/
└── tests/
├── test_exceptions.py # Custom exception handler tests
├── test_validators.py # Shared validator tests
└── test_permissions.py # Shared permission class tests
Apps with test coverage: users, stories, interactions, gamification, media, notifications, reports, tags, common
Frontend tests are co-located with the code they test using a __tests__/ directory convention:
frontend/src/
├── test/
│ └── setup.js # Global test setup (jest-dom matchers)
├── pages/
│ └── __tests__/
│ └── <PageName>.test.jsx # Page-level tests
├── components/
│ └── __tests__/
│ └── <Component>.test.jsx # Component-level tests
└── services/
└── __tests__/
└── <service>.test.js # Service-level tests
Backend: A global conftest.py provides reusable pytest fixtures for common entities (users, stories). Each app may define local helper functions for creating test data specific to its domain.
Frontend: Each test file defines render helpers that wrap components with required providers (e.g., BrowserRouter) and form-fill helpers to reduce boilerplate. API services and router hooks are mocked using vi.mock().
Every Django app must have model tests covering:
| Category | What's Verified |
|---|---|
| Creation | Objects can be created with required fields |
| Defaults | Default values are applied correctly (status, counters, flags) |
| Constraints | Unique constraints, required fields, validation rules |
| Relationships | ForeignKey behavior (CASCADE, SET_NULL), OneToOne links |
| Signals | Side effects triggered on save/delete (e.g., counter updates) |
| Validation |
clean() methods, regex validators, custom validators |
| Ordering | Default queryset ordering |
Serializer tests validate the contract between API input/output and internal models:
- Required vs. optional field enforcement
- Input validation rules (email format, password strength, uniqueness)
- Write-only fields are excluded from responses
- Nested serializer behavior
Service tests verify business logic orchestration:
- Correct model creation and mutation
- Password hashing and token generation
- Error handling for invalid inputs and edge cases
- Anti-enumeration patterns (e.g., login does not reveal whether an email exists)
View tests validate the HTTP contract of each endpoint:
| What to Test | Example |
|---|---|
| Success responses | Correct status code (200, 201, 204) and payload structure |
| Client errors | 400 for validation failures, 409 for conflicts |
| Auth errors | 401 for unauthenticated, 403 for forbidden |
| Data persistence | Created objects exist in the database after POST |
| Security | Sensitive fields (passwords) excluded from responses |
- Every custom permission class is tested with all role combinations (guest, authenticated, owner, admin)
- Custom exception handlers are tested to ensure consistent error response format
- Shared validators (e.g., password strength) are tested with valid, invalid, and edge-case inputs
Each page component is tested for:
| Category | What's Verified |
|---|---|
| Rendering | All expected form fields, buttons, and text render correctly |
| Validation | Client-side validation messages appear for invalid inputs |
| Submission | Successful form submission calls the correct API with correct data |
| Loading State | UI is disabled / shows loading indicator during API calls |
| Error Handling | API error messages are displayed to the user |
| Navigation | Page redirects correctly on success; links navigate to expected routes |
| Accessibility | Error messages use role="alert", elements are queryable by label/role |
Shared UI components are tested for:
- Correct rendering based on props
- User interaction behavior (click, toggle, hover)
- Accessibility attributes (ARIA labels, roles)
-
Mocking Strategy: API service calls are mocked at the module level (
vi.mock); router hooks are mocked to test navigation without real routing -
User Event Simulation: All interactions use
@testing-library/user-eventfor realistic event sequences (type, click, clear) -
Accessibility-First Queries: Tests query elements by
role,labelText, andtextrather than CSS selectors or test IDs
The CI pipeline runs automatically on pushes to main and pull requests targeting main. It consists of two parallel jobs:
┌─────────────────────────────────┐ ┌─────────────────────────────────┐
│ backend-tests │ │ frontend-checks │
│ ───────────── │ │ ──────────────── │
│ • Ubuntu latest, Python 3.12 │ │ • Ubuntu latest, Node.js 20 │
│ • MySQL 8.0 service container │ │ • npm ci │
│ • pip install requirements │ │ • npm run lint │
│ • pytest -v │ │ • npm run test:run │
│ │ │ • npm run build │
└─────────────────────────────────┘ └─────────────────────────────────┘
- All backend tests must pass before a PR can be merged
- All frontend lint checks, tests, and builds must pass before a PR can be merged
- New features must include corresponding tests — PRs without tests will be flagged during review
This section defines the end-to-end user journeys the application must successfully handle. Each scenario represents a critical user flow that must be validated through a combination of unit, integration, and (where applicable) E2E tests.
Actor: Unregistered visitor Goal: Discover local history stories by exploring a map
| Step | Action | Test Approach |
|---|---|---|
| 1 | Guest opens the application | Frontend: page renders without auth |
| 2 | Map view loads with story pins | Backend: story list API returns published stories; Frontend: map component renders pins |
| 3 | Guest clicks a pin to see preview popup | Frontend: popup displays title, place, time |
| 4 | Guest opens the full story page | Backend: story detail API; Frontend: full page renders narrative + metadata |
| 5 | Guest views comments (read-only) | Backend: comment list API; Frontend: comments section visible, submit hidden |
| 6 | Guest switches to Feed view | Frontend: feed renders story cards sorted by most recent |
| 7 | Guest searches by title or location | Backend: search API; Frontend: results update dynamically |
| 8 | Guest filters by time period | Backend: filter API; Frontend: filter controls apply correctly |
Actor: New user Goal: Create an account, log in, and submit a local history story
| Step | Action | Test Approach |
|---|---|---|
| 1 | User opens registration page | Frontend: form renders all fields |
| 2 | User submits valid registration data | Backend: user created (201), password hashed; Frontend: navigates to login |
| 3 | Registration rejected for duplicate email/username | Backend: 409 response; Frontend: error displayed |
| 4 | Registration rejected for weak password | Backend: 400 response; Frontend: validation messages shown |
| 5 | User logs in with credentials | Backend: JWT tokens returned (200); Frontend: navigates to home |
| 6 | Login rejected for wrong credentials | Backend: 401 (anti-enumeration); Frontend: generic error shown |
| 7 | User fills in story submission form | Frontend: title, narrative, map pin, time info fields work |
| 8 | User uploads an image | Backend: media stored; Frontend: preview shown |
| 9 | Story created and visible on map/feed | Backend: story persisted with correct data; Frontend: appears in listings |
Actor: Registered, logged-in user Goal: Engage with stories through likes and comments
| Step | Action | Test Approach |
|---|---|---|
| 1 | User opens a story page | Frontend: interaction buttons visible for authenticated user |
| 2 | User likes a story | Backend: like created, like_count incremented; Frontend: UI reflects liked state |
| 3 | User unlikes the story | Backend: like removed, like_count decremented; Frontend: UI reverts |
| 4 | User submits a comment | Backend: comment created; Frontend: comment appears in list |
| 5 | User deletes own comment | Backend: comment removed; Frontend: comment disappears |
Actor: Registered user Goal: View profile with username, contribution count, and submitted stories
| Step | Action | Test Approach |
|---|---|---|
| 1 | User navigates to profile page | Frontend: profile renders user info |
| 2 | Profile displays username and contribution count | Backend: profile API returns correct data |
| 3 | Profile shows submitted stories list | Backend: user stories API; Frontend: stories render as cards |
| 4 | Privacy settings are respected | Backend: permission checks; Frontend: private fields hidden |
Actor: Logged-in user Goal: Securely end session
| Step | Action | Test Approach |
|---|---|---|
| 1 | User clicks logout | Backend: refresh token blacklisted (205); Frontend: redirected to login |
| 2 | Protected routes inaccessible after logout | Backend: 401 for blacklisted token; Frontend: auth guard redirects |
Actor: Any user (guest or registered) Goal: Find specific stories by title, location, or time period
| Step | Action | Test Approach |
|---|---|---|
| 1 | User enters a search term | Backend: search API filters by title/location; Frontend: results update |
| 2 | User applies time period filter | Backend: filter by year/decade; Frontend: filter controls work |
| 3 | User applies location filter | Backend: location-based filtering; Frontend: map updates |
| 4 | Combined filters return correct results | Backend: multiple filters combine correctly; Frontend: UI consistent |
Every pull request must satisfy these criteria (enforced via PR template):
- Project builds successfully
- Code follows the style guidelines of this project
- Hard-to-understand parts are commented
- Self-review has been performed
- Tests have been added or updated for the change
- New and existing unit tests pass locally
| Criteria |
|---|
| User can register with valid email, username, and password |
| Registration rejects duplicate email/username |
| Registration enforces password strength (8+ chars, uppercase, lowercase, digit) |
| Registration rejects mismatched passwords |
| User can log in with valid credentials and receives JWT tokens |
| Login does not leak user existence (anti-enumeration) |
| User can log out and token is invalidated |
| Protected routes are inaccessible without valid authentication |
| Criteria |
|---|
| Story can be created with title, narrative, location, and time information |
| Story supports all time types (exact year, year range, decade, approximate) |
| Story supports image upload (.jpg, .png, max 2MB) |
| Published stories are visible on the map and feed views |
| Story detail page displays full narrative, metadata, and media |
| Deleting a user does not delete their stories (author set to NULL) |
| Criteria |
|---|
| Registered user can like and unlike a story |
| Like/unlike correctly updates the story's like count |
| Registered user can comment on a story |
| User can delete their own comment |
| Duplicate likes/saves per user are prevented |
| Registered user can save/bookmark a story |
| Criteria |
|---|
| User profile displays username, contribution count, and submitted stories |
| Profile privacy settings control field visibility |
| Only profile owner or admin can edit profile |
| Criteria |
|---|
| Users can search stories by title and location name |
| Users can filter stories by time period (year/decade) |
| Search and filters work across map, feed, and timeline views |
| Criteria |
|---|
| Tags follow format validation (lowercase, hyphens, no spaces/special chars) |
| Stories can have up to 3 tags |
| Users can browse and filter stories by tag |
| Criteria |
|---|
| Points are awarded for publishing stories, receiving likes, comments, and saves |
| Badges are awarded based on defined criteria (registration, milestones) |
| Each user can earn a given badge only once |
| Total points are displayed on the user profile |
| Criteria |
|---|
| Users receive notifications for likes, comments, moderation actions, and badges |
| Notification preferences can be configured per user |
| Unread notification count is displayed |
| Criteria |
|---|
| Users can report stories and comments with a reason |
| A user cannot submit duplicate reports on the same content |
| Admins can review reports and take action (remove content, ban users) |
| Criteria |
|---|
| Image, audio, and video uploads are supported |
| Media files are associated with stories and cascade-deleted with them |
| Media playback works on story detail pages |