Skip to main content

Recruitment Module — QA Test Plan

Author: Senior QA Engineer
Module: Recruitment (Job Requisitions → Job Openings → Job Applicants → Interview Schedules → Job Offers → Staffing Plans)
Type: Functional, Integration, Negative, Boundary, Permission, UI/UX
Environment: Local Docker (docker compose up) or staging


1. Test Strategy

The recruitment module is a sequential pipeline. Every downstream entity depends on an upstream one (an offer cannot exist without an applicant; an applicant cannot exist without an opening). This means:

  • Happy path tests must run in order — you cannot test offers without first creating a requisition → opening → applicant → interview.
  • Each stage must be tested in isolation (CRUD, filters, validation) before testing the full pipeline.
  • Side effects are the highest-risk area — specifically: offer acceptance auto-promoting the applicant to Hired, and applicant creation incrementing the opening's applicationsCount. These must be verified explicitly.
  • Permission boundaries must be tested with a non-admin user — not just assumed from the code.

Priority Order

PriorityArea
P0End-to-end pipeline (smoke test — if this breaks, nothing else matters)
P1Side effects (offer → hired, applicant count)
P1Status transitions (only valid states accepted)
P2Filters and pagination
P2Permission enforcement
P3Validation and error messages
P3Staffing plans (independent of the hire pipeline)

2. Test Environment Setup

Prerequisites

# Start the full stack
docker compose up -d

# Confirm backend is healthy
curl -s http://localhost:8080/actuator/health | jq '.status'
# Expected: "UP"

# Confirm frontend is served
curl -s -o /dev/null -w "%{http_code}" http://localhost:5173
# Expected: 200

Test Accounts Required

Create or confirm these accounts exist before starting. Each must have a distinct permission set.

AccountRole / PermissionsUsed For
hr_adminHR_RECRUITMENT_* (all)Full pipeline tests
hr_viewerHR_RECRUITMENT_VIEW onlyPermission boundary tests
interviewer_userHR_RECRUITMENT_INTERVIEW_MANAGE + VIEWInterview-specific tests
offer_userHR_RECRUITMENT_OFFER_MANAGE + VIEWOffer-specific tests
no_recruitmentAny non-recruitment roleNegative permission tests

Seed Data Required

EntityMinimum needed
DepartmentsAt least 2 (e.g., Engineering, HR)
PositionsAt least 2 (e.g., Software Engineer, HR Specialist)
EmployeesAt least 3 (1 for requested-by, 1 as interviewer, 1 as offer creator)

3. Stage 1 — Job Requisitions

3.1 Happy Path

TC-REQ-001 — Create a requisition (Draft)

  1. Log in as hr_admin.
  2. Navigate to Recruitment → Job Requisitions.
  3. Click New Requisition.
  4. Fill in:
    • Department: Engineering
    • Position: Software Engineer
    • Requested By: any employee
    • Vacancies: 2
    • Estimated Cost: 150000
    • Description: Backend hire for Q3
  5. Submit.

Expected:

  • Row appears in the grid with status Draft.
  • Toast shows "Requisition created".
  • createdAt is today's date.

TC-REQ-002 — Edit a requisition

  1. Find the requisition created in TC-REQ-001.
  2. Click the Edit (pencil) action.
  3. Change Vacancies from 2 to 3.
  4. Save.

Expected:

  • Grid row updates to show 3 vacancies.
  • No duplicate rows appear.

TC-REQ-003 — Status transition: Draft → Open → Approved

  1. On the Draft requisition, click Status.
  2. Set status to Open. Save.
  3. Confirm row moves to the Open tab and disappears from Draft tab.
  4. Click Status again. Set to Approved. Save.
  5. Confirm row is now on the Approved tab.

Expected: Each status change reflects immediately on grid purge. The "All" tab always shows the row regardless of status.


TC-REQ-004 — Status transition: Approved → Filled

Prerequisite: Complete the full pipeline through TC-OFF-005 (offer accepted). Then return here.

  1. After a hire is confirmed, set the requisition status to Filled.

Expected: Row moves to Filled tab. Status badge renders correctly.


TC-REQ-005 — Delete a Draft requisition

  1. Create a second requisition (leave it as Draft).
  2. Click Delete.
  3. Confirm the deletion dialog.

Expected: Row is removed from the grid. A deleted requisition cannot be retrieved by ID (returns 404).


3.2 Column Filters

TC-REQ-006 — Filter by department name

  1. On the All tab, open the column filter on Department.
  2. Type Engineering.

Expected: Only rows with Engineering as department remain. Other rows disappear. Clearing the filter restores all rows.


TC-REQ-007 — Filter by created date range

  1. Open the Created column filter.
  2. Set a date range that includes today.

Expected: Rows within the range shown. Set the "from" date to tomorrow — grid shows zero rows.


TC-REQ-008 — Filter by status (Set filter)

  1. On the All tab, open the Status column filter.
  2. Select only Draft.

Expected: Only draft requisitions shown. This is separate from the tab — the tab shows Approved rows, the column filter narrows within that tab's dataset.


3.3 Negative / Validation

TC-REQ-009 — Submit without required fields

  1. Open New Requisition form.
  2. Leave Department empty. Submit.

Expected: Validation error shown on the Department field. Form does not close.


TC-REQ-010 — Vacancies must be positive

  1. Enter 0 in the Vacancies field. Submit.

Expected: Validation error. Backend returns 400 if client validation is bypassed.


TC-REQ-011 — Cannot skip status (Draft → Approved directly)

This depends on whether the backend enforces state machine rules. Test it.

  1. Set a Draft requisition status directly to Approved via the status dialog.
  2. Check if the backend accepts or rejects it.

Expected (if state machine enforced): 400 Bad Request with a meaningful error message.
Log the actual behavior — if the system accepts it, raise a bug ticket for missing state machine enforcement.


3.4 Permission

TC-REQ-012 — Viewer cannot see New / Edit / Delete

  1. Log in as hr_viewer.
  2. Navigate to Job Requisitions.

Expected: New Requisition button is not visible. Edit, Status, and Delete row actions are not visible. Data is readable.


TC-REQ-013 — Unauthenticated access is blocked

  1. Log out.
  2. Hit GET /api/job-requisitions directly.

Expected: 401 Unauthorized.


4. Stage 2 — Job Openings

4.1 Happy Path

TC-OPEN-001 — Create an opening

  1. Navigate to Recruitment → Job Openings.
  2. Click New Opening.
  3. Fill in:
    • Title: Senior Backend Engineer
    • Description: We are hiring...
    • Salary Range: 80000 to 120000
    • Publish Salary Range: checked
    • Posted Date: today
    • Closing Date: 30 days from today
  4. Submit.

Expected:

  • Row appears with status Open and 0 applicants.
  • Posted and Closing dates render in local date format.

TC-OPEN-002 — Applicant count increments on new applicant

Prerequisite: TC-OPEN-001 complete.

  1. Note the current Applicants count (should be 0).
  2. Create a Job Applicant (TC-APP-001) linked to this opening.
  3. Return to Job Openings.

Expected: The Applicants count for this opening is now 1. This is the auto-increment side effect — verify it is not just a stale cache.


TC-OPEN-003 — Public endpoint returns only Open openings

  1. Set one opening to Closed and one to Open.
  2. GET /api/public/job-openings (no auth token).

Expected: Only the Open opening is returned. The Closed one is excluded. Response is 200 (not 401).


TC-OPEN-004 — Cannot access a Closed opening via public endpoint

  1. GET /api/public/job-openings/{id} where {id} is a Closed opening.

Expected: 404 Not Found. The system must not expose non-open listings publicly.


4.2 Negative

TC-OPEN-005 — Closing date before posted date

  1. Set Closing Date to yesterday when Posted Date is today. Submit.

Expected: Validation error on the closing date field. Log whether this is enforced client-side, server-side, or both.


TC-OPEN-006 — Salary range: From greater than To

  1. Enter Salary From: 150000, Salary To: 80000. Submit.

Expected: Validation error. Log the actual behavior.


5. Stage 3 — Job Applicants

5.1 Happy Path

TC-APP-001 — Create an applicant manually

  1. Navigate to Recruitment → Job Applicants.
  2. Click Add Applicant.
  3. Fill in:
    • Opening: select the opening from TC-OPEN-001
    • First Name: Juan
    • Last Name: dela Cruz
    • Email: [email protected]
    • Phone: 09171234567
  4. Submit.

Expected:

  • Row appears with status Applied and today's appliedDate.
  • Opening's applicationsCount increments (see TC-OPEN-002).

TC-APP-002 — Submit via public application form

  1. POST /api/public/job-applications with payload:
    {
    "openingId": <id from TC-OPEN-001>,
    "firstName": "Maria",
    "lastName": "Santos",
    "email": "[email protected]",
    "phone": "09189999999",
    "coverLetter": "I am very interested in this role."
    }

Expected:

  • 200 OK with the new applicant DTO.
  • Status is Applied.
  • notes field contains the cover letter text.
  • applicationsCount on the opening increments again (now 2).
  • No auth header required.

TC-APP-003 — Status pipeline: Applied → Shortlisted → Interview

  1. Set Juan's status to Shortlisted.
  2. Confirm Juan moves to the Shortlisted tab.
  3. Set status to Interview.
  4. Confirm Juan moves to the Interview tab.

Expected: Each status change is reflected immediately. The applicant does not appear in the previous tab after transition.


TC-APP-004 — Status auto-set to Offered on offer creation

Verified as a side effect in TC-OFF-001. Document here as a cross-reference.

When a JobOffer is created for an applicant, the applicant's status must automatically change from Interview to Offered without HR manually updating it.


TC-APP-005 — Status auto-set to Hired on offer acceptance

Verified as a side effect in TC-OFF-005. Document here as a cross-reference.

When the offer status changes to Accepted, the applicant's status must automatically change to Hired.


5.2 Negative

TC-APP-006 — Duplicate email on same opening

  1. Try to create a second applicant for the same opening with email: [email protected].

Expected: If the backend enforces uniqueness per opening+email, a 400 or 409 is returned. Log the actual behavior — if duplicates are allowed, raise a bug for missing uniqueness constraint.


TC-APP-007 — Apply to a Closed opening

  1. POST /api/public/job-applications with openingId pointing to a Closed opening.

Expected: 400 Bad Request or 422 Unprocessable Entity. Applying to a closed opening should be rejected.
Log the actual behavior — this may not be implemented yet.


TC-APP-008 — Filter by applicant name

  1. Open the All tab on Job Applicants.
  2. Filter the Last Name column with dela Cruz.

Expected: Only Juan appears. Maria does not.


TC-APP-009 — Search filters both first and last name

  1. Use the applicantName filter (or the search param) with value Santos.

Expected: Maria Santos appears. The filter hits both first and last name columns.


6. Stage 4 — Interview Schedules

6.1 Happy Path

TC-INT-001 — Schedule a first-round interview

  1. Navigate to Recruitment → Interview Schedules.
  2. Click Schedule Interview.
  3. Fill in:
    • Applicant: Juan dela Cruz
    • Interviewer: any employee (not Juan)
    • Round: Technical Round 1
    • Scheduled At: tomorrow at 10:00 AM
    • Location: Conference Room A
  4. Submit.

Expected:

  • Row appears with status Scheduled.
  • scheduledAt renders as a full datetime (not just date).

TC-INT-002 — Schedule a second round for the same applicant

  1. Create another interview for Juan with Round: HR Final.

Expected: Two separate rows exist for Juan. The system allows multiple interviews per applicant.


TC-INT-003 — Mark interview as Completed

  1. Set the Round 1 interview status to Completed.

Expected: Row moves to the Completed tab. The feedback action becomes relevant.


TC-INT-004 — Add feedback to a completed interview

  1. On the completed interview, click the Feedback (speech bubble) action.
  2. Add feedback with a rating of 4/5 and recommendation Proceed to next round.
  3. Save.

Expected:

  • Feedback drawer closes without error.
  • Feedback can be retrieved via GET /api/interview-schedules/{id}/feedback.

TC-INT-005 — Mark second interview as No-Show

  1. Set the HR Final interview status to No-Show.

Expected: Row moves to the No-Show tab.


6.2 Negative

TC-INT-006 — Schedule interview in the past

  1. Set Scheduled At to yesterday. Submit.

Expected: Validation error for past scheduling. Log whether this is enforced. If not, raise a bug.


TC-INT-007 — Interviewer cannot interview themselves

  1. Set the applicant and interviewer to the same person (if both have employee records).

Expected: Validation error. Log the actual behavior.


TC-INT-008 — Feedback requires interview to be Completed

  1. Try to add feedback to a Scheduled (not yet completed) interview via API:
    POST /api/interview-schedules/{id}/feedback

Expected: If the backend enforces this, 400 is returned. Log the actual behavior.


TC-INT-009 — Permission: interviewer_user cannot delete interviews

  1. Log in as interviewer_user.
  2. Confirm the Delete action is not visible.
  3. Attempt DELETE /api/interview-schedules/{id} directly.

Expected: 403 Forbidden.


7. Stage 5 — Job Offers

7.1 Happy Path

TC-OFF-001 — Create an offer (side effect: applicant → Offered)

  1. Navigate to Recruitment → Job Offers.
  2. Click Send Offer.
  3. Fill in:
    • Applicant: Juan dela Cruz
    • Position: Software Engineer
    • Offered Salary: 95000
    • Offer Date: today
    • Start Date: 2 weeks from today
    • Expiry Date: 1 week from today
  4. Submit.

Expected:

  • Offer row appears with status Pending.
  • Navigate to Job Applicants — Juan's status must now be Offered (automatic side effect).
  • No manual status update was needed.

TC-OFF-002 — Decline an offer

  1. Set the offer status to Declined.

Expected:

  • Offer moves to Declined tab.
  • Juan's applicant status does not automatically revert — HR must manually update it if needed.
  • Document the actual behavior.

TC-OFF-003 — Withdraw an offer

  1. Create a second offer for Maria Santos (after completing TC-APP-001 through INT-001 for her separately).
  2. Set the offer status to Withdrawn.

Expected: Offer moves to Withdrawn tab.


TC-OFF-004 — Create a second offer for the same applicant

  1. With Juan's first offer in Declined state, try creating a second offer for Juan.

Expected: The system should allow a re-offer after decline. Verify this is possible and that Juan's status re-sets to Offered.


TC-OFF-005 — Accept an offer (side effect: applicant → Hired)

  1. Set Juan's (second) offer status to Accepted.

Expected:

  • Offer moves to Accepted tab.
  • Navigate to Job Applicants — Juan's status is now Hired (automatic side effect). This is the most critical side effect in the entire module.
  • No manual status change was made to the applicant.

7.2 Negative

TC-OFF-006 — Cannot create offer with expiry before offer date

  1. Set Expiry Date to yesterday relative to Offer Date. Submit.

Expected: Validation error. Log whether enforced.


TC-OFF-007 — Cannot create offer with start date before today

  1. Set Start Date to last month. Submit.

Expected: Validation error or warning. Log the actual behavior.


TC-OFF-008 — Offer requires a valid applicant ID

  1. POST /api/job-offers with a non-existent applicantId.

Expected: 404 Not Found with message "Job applicant not found: X".


TC-OFF-009 — Permission: offer_user cannot access Job Requisitions

  1. Log in as offer_user.
  2. Navigate to Job Requisitions.

Expected: Page is inaccessible, or all actions are hidden. API GET /api/job-requisitions should still return data (VIEW permission is separate). Verify what offer_user can and cannot see.


8. Staffing Plans

8.1 Happy Path

TC-SP-001 — Create a staffing plan

  1. Navigate to Recruitment → Staffing Plans.
  2. Click New Plan.
  3. Fill in:
    • Plan Name: FY2026 Q3 Engineering
    • Fiscal Year: 2026
    • Department: Engineering
    • Total Budget: 5000000
    • Created By: any employee
  4. Submit.

Expected:

  • Row appears with status Draft.
  • Department shows Engineering. If no department is selected, it should display Company-wide.

TC-SP-002 — Add plan items

  1. Double-click the plan row (or click View Details row action).
  2. In the detail drawer, add an item:
    • Role Title: Senior Backend Engineer
    • Vacancies: 2
    • Est. Cost: 2000000
  3. Add a second item: Junior QA Engineer, 1, 800000.

Expected: Both items appear in the items grid inside the drawer.


TC-SP-003 — Remove a plan item

  1. In the detail drawer, click the delete icon on the Junior QA Engineer item.

Expected: Item is removed. Senior Backend Engineer item remains.


TC-SP-004 — Status transitions: Draft → Active → Closed

  1. Update the plan status to Active.
  2. Confirm it moves to the Active tab.
  3. Update to Closed.
  4. Confirm it moves to the Closed tab.

TC-SP-005 — Double-click row opens detail drawer

  1. On any tab, double-click a staffing plan row.

Expected: The detail drawer opens showing the plan's metadata and items. This must work the same as clicking the View Details action.


8.2 Filters

TC-SP-006 — Filter by fiscal year

  1. Open the Year column filter.
  2. Enter 2026 (number filter, equals).

Expected: Only plans for fiscal year 2026 appear.


TC-SP-007 — Filter by department name

  1. Filter the Department column with Engineering.

Expected: Only Engineering plans appear. Plans with null department (Company-wide) do not appear.


9. End-to-End Pipeline Test

This test runs the entire hiring pipeline in one sequence. It is the most important test in this document.

TC-E2E-001 — Full pipeline: Requisition to Hired

StepActionVerify
1Create a Job Requisition (Dept: HR, Position: HR Specialist, Vacancies: 1)Status = Draft, row visible in Draft tab
2Move requisition to OpenRow moves to Open tab
3Move requisition to ApprovedRow moves to Approved tab
4Create a Job Opening linked to the requisitionStatus = Open, applicationsCount = 0
5Submit a public application: POST /api/public/job-applicationsStatus = Applied, opening count = 1
6Set applicant status to ShortlistedRow in Shortlisted tab
7Set applicant status to InterviewRow in Interview tab
8Create an Interview Schedule (Round 1)Status = Scheduled
9Mark interview CompletedRow in Completed tab
10Add interview feedbackFeedback visible via GET
11Create a Job Offer for the applicantOffer status = Pending; applicant auto-moves to Offered
12Set offer status to AcceptedOffer in Accepted tab; applicant auto-moves to Hired
13Set requisition status to FilledRow in Filled tab
14Set opening status to FilledRow in Filled tab

Pass criteria: All 14 steps complete without errors. The two automatic side effects (steps 11 and 12) must occur without any manual intervention.


10. Regression Checklist

Run after any change to the recruitment module.

  • Creating a job applicant increments the opening's applicationsCount
  • Creating a job offer sets the applicant status to Offered
  • Accepting a job offer sets the applicant status to Hired
  • Declining/withdrawing an offer does not auto-change the applicant status
  • The All tab always shows all records regardless of status
  • Per-status tabs only show rows matching that status
  • Tab switching does not leak filter state from the previous tab
  • Column filters trigger a fresh data fetch (network request visible in DevTools)
  • Pagination works — page 2 loads different rows than page 1
  • GET /api/public/job-openings returns only Open openings without auth
  • GET /api/job-requisitions returns 401 without auth
  • hr_viewer can read all data but cannot create, update, or delete
  • Status dialog changes persist after closing and reopening the drawer
  • Double-clicking a staffing plan row opens the detail drawer
  • Adding/removing staffing plan items reflects immediately in the drawer grid

11. Bug Report Template

When a test fails, file the bug using this structure:

Title: [MODULE] Short description of the defect

Environment: local / staging / production
Browser: Chrome 124 / Firefox 126
User account: hr_admin / hr_viewer / etc.

Steps to Reproduce:
1.
2.
3.

Expected Result:
<what should have happened>

Actual Result:
<what actually happened>

Severity: Critical / High / Medium / Low
Priority: P0 / P1 / P2 / P3

Evidence:
- Screenshot or screen recording
- Network request/response (from DevTools)
- Backend log snippet if applicable

Severity Guide

SeverityDefinition
CriticalData loss, security bypass, pipeline completely broken
HighA stage of the pipeline cannot be completed, side effect not firing
MediumFilter/sort not working, UI mismatch, wrong status label
LowCosmetic issue, minor label text, non-blocking UX gap

12. Known Risks and Areas to Watch

RiskWhy it mattersHow to test
Side effects breaking on refactorThe Hired auto-promotion is buried inside JobOfferServiceImpl — easy to accidentally breakAlways run TC-OFF-005 after any service change
Filter JOINs causing duplicate rowsLEFT JOINs for name filters can multiply rows if the relation is one-to-manyCount rows before and after applying a name filter; totals must match
Concurrent offer acceptanceTwo HR users accepting two offers for the same applicant simultaneously could set status twiceSimulate with two browser tabs if load testing is not available
Stale grid after mutationGrid purge might not fire if the dialog closes before the mutation settlesAdd a delay between submitting and checking — observe the network request order
Public endpoint exposing draft openingsA regression could accidentally expose non-Open openingsTC-OPEN-003 and TC-OPEN-004 must run on every deploy
Missing pagination on staffing plansThe old implementation returned a flat list; the new one paginates — old clients or tests may breakVerify GET /api/staffing-plans returns a Page object, not an array