A trimmed, BD-market-focused manual + API QA curriculum, expanded to 200 hours with real hands-on Playwright automation, OS/networking fundamentals, and a scratch-to-advanced SQL track built for both QA data validation and RCM data-analyst roles (e.g. Commure/Augmedix Dhaka) — built to get you hired quickly, with self-check drills so you walk into the exam room unstoppable.
Use the tabs at the top to move between stages, and the round chips below them to jump between modules inside a stage. Every module keeps its original priority badge — Must Master Must Know Should Know Awareness — so you always know where to spend your limited hours. The Drill tab is a standalone self-check flashcard deck pulling every "memorize cold" fact from across the guide.
The original trimmed guide deliberately cut hands-on automation to stay inside ~125 hours. With 200 hours available, that trade-off no longer applies — this edition adds a full Automation module (Page Object Model, a CI pipeline) and a CS Fundamentals module (OS + networking, the layer just below what Part 2 already teaches) on top of the complete original curriculum. Nothing from the original was removed.
Three targeted updates: (1) SQL (Part 12) was rewritten start-to-finish — it now ramps from absolute scratch through window functions, CTEs, recursive CTEs, correlated subqueries, reconciliation/anomaly queries, and indexes/EXPLAIN, using a healthcare RCM (claims/payments/denials) schema, specifically to also prepare you for RCM Data Analyst roles like Commure/Augmedix Dhaka — not just SQA roles. (2) The Automation module is now Playwright-only instead of Selenium + Playwright, since splitting hands-on time across two tools was producing shallow knowledge of both; a short awareness-level Selenium vocabulary mapping was kept so you're not blindsided if a specific job asks for it. (3) The Roadmap's hour totals and schedule (1.1/1.2) were corrected — they were still showing stale pre-expansion numbers — and the tab/chip highlight colors were simplified to a plain white background with black text.
You are not trying to become a career QA engineer. You are trying to:
Every decision in this guide — what's included, what's cut, how deep each topic goes — was made against that goal. This is not the same guide I'd write for someone who wants to become a Senior SQA Automation Engineer or SDET for the next ten years. If that ever becomes your actual goal, this guide should be treated as a foundation, not the ceiling.
The original ask (see your own notes, if you kept them) covered a much larger 250–350 hour curriculum including deep Selenium framework architecture, JMeter performance engineering, a Therap-specific mock exam, a 60–80 problem LeetCode grind, and an exhaustive 30-object test-case scenario library. Here's what changed and why:
| Cut/trimmed | Why |
|---|---|
| Selenium framework building (POM, pytest architecture, CI integration) | You confirmed most broad BD QA roles — and Therap specifically — don't require it at entry level. Hands-on Selenium takes 40–60 hours to do properly. That's the single biggest time cost removed. You'll still get a conceptual chapter so you're never blindsided if it comes up. |
| JMeter hands-on performance engineering | Same logic — kept as a short conceptual chapter, not a hands-on module. |
| Deep security/pentesting depth | Kept as awareness-level only (what a junior QA is expected to recognize, not exploit). |
| 60–80 problem LeetCode grind | Trimmed to 15–20 carefully chosen problems — enough to survive a basic screening round. Real DSA depth is deferred to your backend-dev interview prep later, where it actually matters more. |
| Therap-specific mock exam | You're targeting BD broadly, not just Therap. You still get a general BD-style written mock exam (many BD companies use a similar shape: SQL + scenario writing + a bit of logic/coding), just not framed as a leaked/specific Therap simulation. |
| 30-object test case scenario library | Trimmed to ~14 well-chosen, genuinely diverse objects. The thinking framework is what matters — once you have it, you can apply it to any 31st object an interviewer throws at you. |
| 3rd portfolio project (Selenium) | Dropped. You get 2 strong, complete portfolio projects (Manual QA + API Testing) instead of 3 mediocre ones. |
What was NOT cut, and was actually deepened relative to a "pure QA" guide:
Every topic is tagged with one of these. Spend your limited hours accordingly.
| Tag | Meaning | Time strategy |
|---|---|---|
| 🔴 MUST MASTER | You will be tested on this in nearly every BD QA interview or written test. Weakness here disqualifies you. | Practice until automatic. Don't just read — do the exercises. |
| 🟠 MUST KNOW | Commonly asked; expected working knowledge. | Understand deeply, practice a moderate amount. |
| 🟡 SHOULD KNOW | Comes up often enough to matter; differentiates you from weaker candidates. | Understand the concept and one worked example. Don't over-invest. |
| ⚪ NICE TO HAVE / AWARENESS ONLY | Rarely required at entry level for the roles you're targeting; included so you're never caught flat-footed. | Read once. Don't practice extensively — this is explicitly what we trimmed. |
Where this guide states something is "commonly expected in BD QA hiring," that reflects a synthesis of widely-reported patterns across junior QA job postings and public interview-experience discussions (BDJobs, LinkedIn, Glassdoor-style writeups, QA community forums) — not a single verified source per claim, and not insider/leaked material from any specific company including Therap. Where something is industry-standard testing theory (ISTQB-adjacent concepts like BVA, EP, severity/priority), that's standard and stable regardless of geography. The guide flags the difference inline where it matters (e.g., "this exact wording is industry-standard; the emphasis on it in interviews is a BD-market pattern").
Total: ~203 hours (the "200-Hour Edition"), organized into 6 levels that match the tabs at the top of this page. This table replaces an earlier draft that still showed the pre-expansion ~120-130 hour numbers — those were stale; this is the real total across every module currently in the guide, including Playwright automation and CS Fundamentals.
| Level | Tabs covered | Focus | Hours | Cumulative |
|---|---|---|---|---|
| Level 1 | Foundations, Concepts | Software fundamentals, SDLC/STLC, testing fundamentals & types | 24h | 24h |
| Level 2 | Test Design | Test design techniques, test case design, scenario testing, bug reporting | 24h | 48h |
| Level 3 | Tools, SQL | Jira, Linux, Git, and the full scratch-to-advanced SQL track (12.1-12.33) | 45h | 93h |
| Level 4 | API & Python, Automation | API testing, Postman, Python for QA, Playwright automation | 48h | 141h |
| Level 5 | CS Fundamentals, Pro Skills | OS & networking, awareness chapter, light DSA, Agile/communication, QA metrics | 35h | 176h |
| Level 6 | Interview Prep, Portfolio | Interview bank, mock exams, both portfolio projects | 27h | 203h |
At 2 hrs/day, 6 days/week (assume 1 rest day — burnout kills consistency more than pace does), that's ~12 hours/week. 203 hours ÷ 12 ≈ 17 weeks. This is longer than the guide's earlier 10-week estimate — that estimate was written before the Playwright, CS Fundamentals, and expanded SQL modules existed. 17 weeks is the honest number for everything currently in this guide, top to bottom.
| Week | Dates (approx.) | Focus | Hours |
|---|---|---|---|
| 1 | Sept 25 – Oct 1 | Software fundamentals, SDLC, start STLC | 12h |
| 2 | Oct 2 – Oct 8 | Finish STLC, Testing Fundamentals, Types of Testing | 12h |
| 3 | Oct 9 – Oct 15 | Test Design Techniques, start Test Case Design | 12h |
| 4 | Oct 16 – Oct 22 | Finish Test Case Design, Scenario Testing, Bug Reporting | 12h |
| 5 | Oct 23 – Oct 29 | Jira, Linux, Git, begin SQL (12.2-12.9: basics through conditional aggregation) | 12h |
| 6 | Oct 30 – Nov 5 | SQL — joins, self joins, subqueries, correlated subqueries, EXISTS (12.10-12.14) | 12h |
| 7 | Nov 6 – Nov 12 | SQL — CTEs, recursive CTEs, window functions intro, ROW_NUMBER/RANK (12.15-12.19) | 12h |
| 8 | Nov 13 – Nov 19 | SQL — LAG/LEAD, dates, strings, NULLs, reconciliation & anomalies, indexes/EXPLAIN, then start API Testing (12.20-12.33) | 12h |
| 9 | Nov 20 – Nov 26 | API Testing, Postman project, begin Python | 12h |
| 10 | Nov 27 – Dec 3 | Python for QA (core) | 12h |
| 11 | Dec 4 – Dec 10 | Finish Python, begin Playwright automation | 12h |
| 12 | Dec 11 – Dec 17 | Playwright — locators, auto-waiting, POM, CI pipeline | 12h |
| 13 | Dec 18 – Dec 24 | OS & Networking fundamentals | 12h |
| 14 | Dec 25 – Dec 31 | Finish OS/Networking, Awareness chapter, begin Light DSA | 12h |
| 15 | Jan 1 – Jan 7 | Finish DSA, Agile/Communication, QA Metrics, begin Interview Prep | 12h |
| 16 | Jan 8 – Jan 14 | Interview Prep & Mock Exams, begin Portfolio Projects | 12h |
| 17 | Jan 15 – Jan 21 | Finish Portfolio Projects, apply for jobs | 11h |
Target: applying to jobs in earnest around January 18-22, 2027, roughly 17 weeks after you start. Interview cycles in BD can add another 2–6 weeks on top of that before an offer, so a realistic "employed" target is February–April 2027. That then starts your 4–5 month Django clock, landing a backend transition target around June–September 2027.
If you can occasionally do 3 hrs instead of 2 on weekends, you can compress this by 2-3 weeks. Don't force it on weekdays — 2 focused hours beats 1 exhausted hour plus 1 distracted hour. If you're specifically targeting the Commure/Augmedix RCM Data Analyst role, it's reasonable to prioritize finishing SQL (Level 3) ahead of schedule even if that means moving faster through lower-priority Awareness-tagged content later — see 0.3's priority tags.
| Practice (do it repeatedly, build muscle memory) | Memorize (know cold, recall instantly) |
|---|---|
| Writing test cases from a spec | Severity vs. Priority definitions |
| Writing SQL queries from scratch — especially joins, CTEs, and window functions | Defect lifecycle states and order |
| Applying BVA/EP to a new input field | HTTP status code meanings (200s/300s/400s/500s) |
| "How would you test X" on objects you've never seen | SDLC/STLC phase names and order |
| Writing bug reports from a bug description | Core testing terminology (verification vs validation, etc.) |
| Reading and debugging someone else's SQL/Python | ROW_NUMBER vs RANK vs DENSE_RANK behavior on ties |
Rule of thumb: anything that requires judgment or construction, you practice. Anything that's a fixed definition or fact, you memorize with flashcards/repetition and move on — don't over-study static facts.
| Level | Milestone | What "done" looks like |
|---|---|---|
| Level 1 | Basic QA understanding | You can explain SDLC, STLC, and 15+ core QA terms correctly without notes. |
| Level 2 | Junior manual QA readiness | You can write a 10-step professional test case and a professional bug report from a vague description, unaided. |
| Level 3 | Strong SQA + RCM analyst candidate | You can write joins, CTEs, and window functions from scratch, build an aging/reconciliation report unaided, and navigate Linux/Git comfortably. |
| Level 4 | QA + API + automation candidate | You can design and execute a full Postman test collection including negative/boundary cases, read/write basic Python, and have a working Playwright suite with CI. |
| Level 5 | Well-rounded technical candidate | You can explain OS/networking fundamentals cleanly and hold your own in an Agile/Scrum conversation. |
| Level 6 | Interview-ready | You've completed the mock written exam under time pressure, done both mock interviews, and have 2 portfolio projects on GitHub. |
flowchart TD
A[Software Fundamentals] --> B[SDLC / STLC]
B --> C[Testing Fundamentals & Types]
C --> D[Test Design Techniques]
D --> E[Test Case Writing]
E --> F[Bug Reporting + Jira]
C --> G[SQL: Scratch to Advanced]
A --> H[API Testing Concepts]
H --> I[Postman Project]
G --> J[Linux + Git]
F --> J
I --> K[Python for QA]
J --> K
K --> L[Playwright Automation]
K --> M2[Light DSA]
L --> T[CS Fundamentals: OS & Networking]
E --> M[Scenario Practice / How-would-you-test-X]
F --> M
T --> N[Interview Prep + Mock Exam]
M2 --> N
M --> N
I --> N
N --> O[Portfolio Projects]
O --> P[Apply for Jobs]
P --> Q[QA or RCM Analyst Job Secured]
Q --> R[Django Learning Track — 4-5 months]
R --> S[Backend Developer Transition]
Dual-purpose: this is also backend-dev groundwork.
Software is instructions running on hardware to produce useful behavior. As a QA engineer you almost never test "software" in the abstract — you test applications: web apps, mobile apps, or APIs, each with predictable moving parts. Understanding those parts is what turns you from someone who can click around and notice things look wrong into someone who can reason about where a bug is likely to live.
sequenceDiagram
participant U as User (Browser/App)
participant C as Client (Frontend)
participant S as Server (Backend)
participant D as Database
U->>C: Clicks "Login"
C->>S: HTTP POST /api/login {email, password}
S->>D: SELECT * FROM users WHERE email = ?
D-->>S: User row (hashed password)
S->>S: Compare password hash
S-->>C: 200 OK + auth token
C-->>U: Redirect to dashboard
| Frontend | Backend | |
|---|---|---|
| Runs where | User's browser/device | Server |
| Typical tech | HTML, CSS, JavaScript, React/Vue | Python/Django, Node, Java, databases |
| What QA tests | Layout, interactions, validation messages, responsiveness | Business logic, data correctness, APIs, performance |
Web apps run in a browser (no install). Mobile apps are installed (native — Swift/Kotlin, or cross-platform — React Native/Flutter). As QA you'll mostly test web apps and their APIs at entry level in BD; mobile testing (if it comes up) adds device/OS fragmentation as a new axis of testing (see Part 6, compatibility testing).
An API (Application Programming Interface) is a defined contract that lets one piece of software ask another for data or action, without needing to know how the other one works internally. A REST API is the dominant style: it uses HTTP, treats things as "resources" (e.g., /users, /orders/42), and uses HTTP methods to say what you want to do to that resource. You'll go deep on this in Part 15 — for now, just internalize: the API is the backend's front door, and testing it directly (skipping the UI) is often faster and more precise than clicking through screens.
HTTP (HyperText Transfer Protocol) is the language browsers and servers speak to each other. HTTPS is the same thing, encrypted (via TLS/SSL) — this matters for security testing (Part 19) because unencrypted HTTP means anyone on the network can read the traffic, including passwords.
Every interaction is a request (from client) and a response (from server):
Request:
GET /api/products/17 HTTP/1.1
Host: shop.example.com
Authorization: Bearer eyJhbGciOi...
Response:
HTTP/1.1 200 OK
Content-Type: application/json
{"id": 17, "name": "Wireless Mouse", "price": 850, "in_stock": true}
| Range | Meaning | Examples you must know |
|---|---|---|
| 2xx | Success | 200 OK, 201 Created, 204 No Content |
| 3xx | Redirection | 301 Moved Permanently, 304 Not Modified |
| 4xx | Client error (you did something wrong) | 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 409 Conflict, 422 Unprocessable Entity |
| 5xx | Server error (the backend broke) | 500 Internal Server Error, 502 Bad Gateway, 503 Service Unavailable |
401 (Unauthorized) actually means "not authenticated" — you don't know who this is. 403 (Forbidden) means "I know who you are, and you're not allowed." Mixing these up is a very common junior mistake and interviewers know to probe it.
The standard data format for APIs. Key-value pairs, nested objects, arrays:
{
"user": {
"id": 5,
"name": "Shahed",
"roles": ["tester", "admin"],
"active": true,
"last_login": null
}
}
You need to read this fluently — API testing is 80% "does the JSON response match what it should."
You don't need deep networking, but know: DNS resolves a domain name to an IP address; a port identifies a specific service on a server (80 = HTTP, 443 = HTTPS); localhost means your own machine. In the browser, know how to open DevTools (F12) — the Network tab (see actual requests/responses/status codes — this is your best friend for both manual and API-adjacent testing) and the Console tab (see JS errors).
https://jsonplaceholder.typicode.com/users/1) and write down what each field means and what data type it is.| Model | Shape | QA's role |
|---|---|---|
| Waterfall | Sequential: Requirements → Design → Development → Testing → Deployment → Maintenance. Each phase finishes before the next starts. | Testing happens only after development is "done" — late, risky, expensive to fix bugs. |
| V-Model | Waterfall's twin, but each dev phase pairs with a matching test phase planned in parallel (e.g., requirements ↔ acceptance testing, design ↔ system testing). | Testing is planned early even if executed late. |
| Iterative | Build in repeated cycles, each producing a working (if incomplete) version. | Testing happens each cycle, catching issues earlier than Waterfall. |
| Agile/Scrum | Short, fixed-length iterations ("sprints," usually 1-2 weeks) delivering working increments continuously. | Testing is continuous, embedded in every sprint. This is what you'll encounter at almost every BD software company. |
flowchart LR
subgraph Waterfall
A1[Requirements] --> A2[Design] --> A3[Development] --> A4[Testing] --> A5[Deployment] --> A6[Maintenance]
end
flowchart TD
subgraph Scrum Sprint Cycle
B1[Sprint Planning] --> B2[Daily Standups + Development + Testing]
B2 --> B3[Sprint Review]
B3 --> B4[Sprint Retrospective]
B4 --> B1
end
| Term | Meaning |
|---|---|
| User story | A feature described from the user's perspective: "As a [user], I want [goal], so that [reason]." |
| Acceptance criteria | Specific, testable conditions a story must meet to be considered done. This is literally the seed of your test cases. |
| Backlog | The full list of stories/tasks not yet completed, prioritized. |
| Sprint | A fixed time-box (commonly 1-2 weeks) in which a set of backlog items are built and tested. |
| Sprint planning | Meeting at sprint start: what will we build this sprint? |
| Daily standup | Short daily sync: what I did, what I'll do, blockers. |
| Sprint review | End-of-sprint demo of what was built. |
| Retrospective | End-of-sprint meeting: what went well, what didn't, what to improve. |
| Definition of Ready (DoR) | Criteria a story must meet before work starts (e.g., acceptance criteria written, designs attached). |
| Definition of Done (DoD) | Criteria before a story is considered complete (e.g., code reviewed, tests passed, no critical bugs open). |
The principle of moving testing activities as early as possible in the SDLC — reviewing requirements for testability, writing test cases before/during development instead of after, testing each piece as it's built rather than waiting for a "testing phase." This is the dominant modern industry philosophy and interviewers like to hear you understand why: bugs found in requirements cost far less to fix than bugs found in production (commonly cited rule of thumb: cost roughly multiplies at each later stage).
"As a registered user, I want to reset my password via email, so that I can regain access if I forget it."
Notice each bullet is already almost a test case title. This is exactly how professional QAs work — acceptance criteria are the primary input to test design, not an afterthought.
flowchart LR
R[Requirement Analysis] --> P[Test Planning]
P --> D[Test Case Development]
D --> E[Test Environment Setup]
E --> X[Test Execution]
X --> C[Test Closure]
| Phase | What happens | Key deliverable |
|---|---|---|
| Requirement Analysis | Understand what's being built; identify testable vs. non-testable requirements; ask clarifying questions. | List of queries/clarifications; testability assessment |
| Test Planning | Decide scope, approach, resources, schedule, entry/exit criteria. | Test Plan document |
| Test Case Development | Write test cases/scenarios, prepare test data. | Test cases, test data |
| Test Environment Setup | Prepare the system under test (staging server, test DB, test accounts). | Confirmed working environment |
| Test Execution | Run the test cases, log actual results, raise bugs for failures. | Execution results, defect reports |
| Test Closure | Summarize results, evaluate exit criteria, document lessons learned. | Test summary/closure report |
Interviewers sometimes ask "when do you stop testing?" — the honest professional answer is never purely by feeling; you define exit criteria up front and check against them, while acknowledging in practice teams also weigh time/budget/risk.
"Users can filter a product list by price range on an e-commerce site."
Take this vague requirement: "Add a 'Remember Me' checkbox to the login page." Write down 5 clarifying questions you'd ask before test design, and then draft 3 test case titles assuming reasonable answers to your own questions.
| Term | Scope | Focus |
|---|---|---|
| Quality Assurance (QA) | Process-oriented, whole project | Preventing defects by improving the process itself |
| Quality Control (QC) | Product-oriented | Identifying defects in the actual product |
| Testing | A subset of QC | The specific activity of executing software to find defects |
In real BD job postings, "SQA Engineer" almost always means what is technically QC + Testing work day-to-day, despite the title. Don't get hung up on the terminology mismatch in job titles — but do know the correct definitions cold, because interviewers ask this specific question extremely often as a warm-up.
Classic interview one-liner: verification is static, validation is dynamic.
flowchart LR
E[Human Error
mistake by developer/analyst] --> D[Defect / Fault
flaw in the code]
D --> F[Failure
observed wrong behavior when executed]
A defect can exist without a failure (dead code path never executed). Know this distinction — it's a favorite trick question.
| Term | Definition |
|---|---|
| Test condition | An item or event that could be verified (e.g., "login with valid credentials") |
| Test scenario | A high-level "what to test" — one line, no steps (e.g., "Verify user can log in successfully") |
| Test case | Detailed steps, data, and expected results for a specific scenario |
| Test suite | A collection of related test cases |
| Test plan | The document describing scope, approach, resources, schedule for testing |
| Test script | Step-by-step instructions (manual) or automated code implementing a test case |
| Test data | The specific input values used during execution |
| Test environment | The hardware/software setup where tests run (staging server, test DB, browsers, devices) |
Scenario vs. Test Case — the #1 confused pair. A scenario is what. A test case is how (detailed steps + expected result). One scenario often expands into multiple test cases (positive, negative, boundary variants).
| Manual | Automation | |
|---|---|---|
| Who/what executes | A human | A script/tool |
| Best for | Exploratory testing, usability, one-off/changing features, early-stage features | Repetitive regression, stable features, high-volume data checks |
| BD entry-level reality | This is what you'll do 80-90% of the time in your first QA job | Nice to have, rarely the sole job of a true junior/fresher role |
| Type | Tester knows internal code? | Typical tester |
|---|---|---|
| Black-box | No — tests based on inputs/outputs only | Manual/functional QA (you, day-to-day) |
| White-box | Yes — tests internal logic/code paths | Developers, unit testers |
| Grey-box | Partial knowledge | QA who also reads some code/DB schema (common for API/DB testing) |
A shockingly large share of real bugs live in negative testing — junior candidates who only think positive are immediately identifiable as weak.
For each: definition → purpose → example → common confusion.
flowchart TD
U["UI / E2E Tests
(few, slow, expensive)"]
I["Integration Tests
(some, medium speed)"]
Un["Unit Tests
(many, fast, cheap)"]
U --- I --- Un
As a manual/API QA, you'll live mostly at the top two layers. Developers own the unit layer. Knowing this pyramid exists — and that it's a best-practice ideal often not fully followed in real BD companies — is enough at entry level.
| Type | Tests what | Who usually does it | Example |
|---|---|---|---|
| Unit testing | Smallest piece of code (a function) in isolation | Developers | Testing a calculate_discount() function |
| Integration testing | Interaction between two or more modules/components | Devs + QA | Testing that the payment module correctly talks to the inventory module |
| System testing | The whole integrated system, end to end | QA | Full checkout flow: browse → cart → pay → order confirmation |
| Acceptance testing (UAT) | Whether the system meets business/user needs, often done by the client/business | Client/business users, sometimes QA-facilitated | Client verifies the new invoicing feature matches their actual workflow |
| Type | Definition | When used |
|---|---|---|
| Smoke testing | Quick, shallow check that the build is stable enough to test further ("is it even worth testing today?") | Immediately after a new build/deployment |
| Sanity testing | Narrow, focused check that a specific fix/feature works, without full regression | After a small bug fix, before deeper testing |
| Regression testing | Re-running existing tests to confirm previously-working functionality still works after a change | After any code change, especially before release |
| Retesting | Specifically re-testing a previously failed test case after the fix, to confirm that exact bug is resolved | After a bug is marked "Fixed" |
Smoke = broad but shallow, done first, on a new build. Sanity = narrow but deeper, done after a targeted fix. Regression = broad, done to catch side effects. Retest = specifically re-checking the exact bug that was reported.
| Type | Definition |
|---|---|
| Exploratory testing | Simultaneous learning, test design, and execution — unscripted but disciplined and purposeful, guided by a tester's experience |
| Ad-hoc testing | Informal, unstructured testing without any plan or documentation — closer to "random poking" than exploratory testing's disciplined approach |
| End-to-end testing | Testing a complete real-world user workflow from start to finish across all integrated systems |
| Type | Definition |
|---|---|
| Cross-browser testing | Does it work correctly on Chrome, Firefox, Safari, Edge? |
| Cross-platform testing | Does it work across OSes/devices (Windows/Mac/Linux, iOS/Android)? |
| Compatibility testing | Umbrella term covering both, plus different versions/screen sizes/resolutions |
| Usability testing | Is it intuitive and pleasant to use? |
| Accessibility testing | Can users with disabilities use it (screen readers, keyboard navigation, color contrast)? |
| Localization testing (L10n) | Does it work correctly for a specific locale (translated text, date/currency formats)? |
| Internationalization testing (I18n) | Is the underlying app architecturally capable of supporting multiple locales (before actual translation)? |
| Type | Definition |
|---|---|
| Installation testing | Does the software install/uninstall/upgrade correctly? |
| Recovery testing | Does the system recover gracefully after a crash/power failure? |
| Reliability testing | Does the system perform consistently over an extended period without failure? |
| Type | Definition |
|---|---|
| Performance testing | Umbrella term — how the system behaves under a workload (speed, stability, scalability) |
| Load testing | Behavior under expected/normal user load |
| Stress testing | Behavior under load beyond normal capacity, to find the breaking point |
| Volume testing | Behavior with a large amount of data (not necessarily many users) |
| Security testing | Umbrella term — see Part 19 |
This is the single highest-leverage section for both written tests and interviews — nearly every BD QA written test includes an EP/BVA question.
Divide input data into partitions (groups) where all values in a partition should behave the same way. Instead of testing every possible input, test one representative value per partition — this is the core efficiency argument for EP.
A field accepts age 18–60.
Test one value from each: e.g., 10 (invalid), 35 (valid), 75 (invalid) — 3 tests instead of exhaustively testing every age.
A discount code field that requires alphanumeric, 6-8 characters.
An e-commerce quantity selector accepts 1–10 items per order. | Partition | Representative value | Expected | |---|---|---| | Invalid (below) | 0 | Rejected | | Valid | 5 | Accepted | | Invalid (above) | 15 | Rejected | | Invalid (type) | "abc" | Rejected |
A password field requires 8-20 characters. Identify the partitions and one representative test value for each. (Answer at the end of this section.)
Bugs cluster at the edges of partitions, not in the middle — off-by-one errors are the most common defect type in real software. BVA tests the boundary values directly: the minimum, just below minimum, just above minimum, the maximum, just below maximum, just above maximum.
Same age field, 18–60. | Test value | Boundary | Expected | |---|---|---| | 17 | just below min | Rejected | | 18 | min | Accepted | | 19 | just above min | Accepted | | 59 | just below max | Accepted | | 60 | max | Accepted | | 61 | just above max | Rejected |
File upload accepts max 5MB.
if size > 5MB (correct) vs if size >= 5MB (off-by-one bug that rejects exactly-5MB files that should be allowed). BVA is the technique for catching this class of bug.EP + BVA together is how professional QAs actually design test cases for any bounded input — EP gives you the categories, BVA gives you the sharpest values within them.
A discount system gives 10% off for orders ৳500–৳2000. Design BVA test cases. (Answer at the end.)
Used when output depends on a combination of multiple input conditions. Lay out all combinations of conditions as columns, and the resulting action as the last row.
A loan approval system — approve if (Credit Score ≥ 700) AND (Income ≥ ৳30,000). Otherwise reject, unless there's a co-signer, in which case a lower bar applies.
| Condition | Rule 1 | Rule 2 | Rule 3 | Rule 4 |
|---|---|---|---|---|
| Credit Score ≥ 700 | Y | Y | N | N |
| Income ≥ 30,000 | Y | N | Y | N |
| Has co-signer | – | Y | Y | N |
| Action: Approve | ✅ | ✅ | ✅ | ❌ |
Each column becomes one test case. This technique is exactly what interviewers mean by "how would you test a feature with multiple business rules interacting."
Used when the system's behavior depends on its current state and moves to a new state based on an event. Model as states + transitions.
An order's lifecycle — Placed → Confirmed → Shipped → Delivered, with a Cancelled state reachable from Placed or Confirmed but NOT from Shipped.
stateDiagram-v2
[*] --> Placed
Placed --> Confirmed
Placed --> Cancelled
Confirmed --> Shipped
Confirmed --> Cancelled
Shipped --> Delivered
Shipped --> Cancelled: INVALID (should be blocked)
Good state transition tests specifically include invalid transitions — e.g., "verify a Shipped order cannot be cancelled" is often the highest-value test case, because that's exactly the transition developers forget to guard against.
When multiple independent variables combine (e.g., Browser × OS × Login type), testing every combination explodes exponentially. Pairwise testing selects a much smaller subset that still covers every pair of values at least once — based on the empirical observation that most real bugs are triggered by interactions between just two parameters.
3 browsers × 3 OSes × 2 login types = 18 full combinations, but a pairwise-designed set can cover all pairs in about 9 test cases. (Free tools like PICT can generate these — awareness-level only for you; understanding why it's used is what's tested, not tool proficiency.)
A more formal, visual precursor to decision tables — mapping causes (inputs) to effects (outputs) with logical operators (AND/OR/NOT), then converting the graph into a decision table. In practice, most BD interviews just want you to produce the decision table directly; know this term exists and that it's the theoretical basis for decision tables.
Using experience and intuition to guess where bugs are likely to hide — no formal technique, just informed pattern recognition (empty fields, extremely long strings, special characters, double-clicking a submit button, going back in browser history mid-transaction).
Using a pre-built checklist of common concerns (e.g., a standard "form validation checklist") to guide testing quickly, especially useful under time pressure.
Prioritizing testing effort based on the probability a feature will fail combined with the impact if it does. A payment feature (high impact, moderate probability) gets more testing attention than a footer link color (low impact, low probability), even under time pressure.
| Low impact | High impact | |
|---|---|---|
| Low probability | Test last / skip if no time | Test with moderate priority |
| High probability | Test with moderate priority | Test first, most thoroughly |
Already introduced in Part 6 — here it's worth noting it's a formal technique, not just "clicking around": experienced testers use structured approaches like session-based test management (time-boxed, documented exploration sessions with a specific charter/goal) to make it disciplined and reportable rather than ad-hoc.
Invalid-short (<8, e.g. 5 chars), Valid (8-20, e.g. 12 chars), Invalid-long (>20, e.g. 25 chars). If special-character rules exist, that's an additional partition axis.
৳499 (just below min, no discount), ৳500 (min, discount applies), ৳501 (just above min, discount applies), ৳1999 (just below max, discount applies), ৳2000 (max, discount applies), ৳2001 (just above max, discount should NOT apply — verify this exact boundary carefully, it's where off-by-one bugs hide).
| Field | Purpose |
|---|---|
| Test Case ID | Unique identifier (e.g., TC_LOGIN_001) |
| Title | Short, specific description of what's being verified |
| Preconditions | What must be true before execution starts |
| Test Data | Specific input values to use |
| Steps | Numbered, unambiguous actions |
| Expected Result | What should happen — precise, verifiable |
| Actual Result | Filled in during execution |
| Status | Pass/Fail/Blocked |
| Priority | Business importance (High/Medium/Low) |
| Severity | Impact of the defect if this fails (Critical/Major/Minor/Trivial) — technically a bug-report field, but experienced testers estimate it during design too |
| Environment | Browser, OS, device, build version |
| Attachments/Evidence | Screenshots, logs |
❌ Bad:
Title: Test login
Steps: Try to log in
Expected: It should work
✅ Good:
TC_LOGIN_002 — Verify login fails with incorrect password
Preconditions: User account test@example.com exists and is active.
Test Data: Email: test@example.com, Password: WrongPass123
Steps:
1. Navigate to the login page.
2. Enter test@example.com in the Email field.
3. Enter WrongPass123 in the Password field.
4. Click "Login".
Expected Result: Login is rejected. An error message "Incorrect email or password" is displayed. User remains on the login page. No session/token is created.
Priority: High | Severity if fails: Major
The difference: the bad version is unrepeatable and unverifiable — two different testers would do different things and disagree on "did it pass." The good version is precise enough that anyone could execute it identically and get the same verdict.
"Should show an error" is weak. "Should show the error message 'Incorrect email or password', not a generic 500 error page, and should NOT reveal whether the email exists" is what a strong QA engineer writes — because that last clause is testing a real security concern (user enumeration).
Before writing any test case, run this checklist mentally for any input field or feature:
This 8-point checklist is your general-purpose "how do I not miss anything" tool — use it below.
Below are 14 objects/features. For each, a short worked example shows the systematic thought process (not just answers) — apply the 8-point checklist above. These deliberately span software UI, physical objects (a favorite BD-interview curveball to test raw testing thinking, decoupled from any tech knowledge), and backend-adjacent features.
' OR '1'='1); case-sensitivity of email.abc@, abc.com).5 + + 3)..exe renamed .jpg); empty file (0 bytes).Password strength meter, image upload, messaging application (read receipts, message ordering), Facebook-like feed (infinite scroll, like/unlike toggling), microwave, vending machine, USB cable, water bottle. Write at least 5 test scenarios each using the 8-point checklist before moving on.
When asked "how would you test X," don't just list random ideas. Structure your answer:
Attempt each of these on paper/in your head using the framework above, before reading the model answers section further below. This is the single highest-value exercise in this entire guide for interview performance — don't skip the "attempt first" step.
Clarify — hardware, software, or both? Functional: calls, texts, camera, apps launch. Negative: low battery behavior, no SIM, airplane mode edge cases. Boundary: max storage full, max app count. Usability: screen responsiveness, button layout. Compatibility: different chargers/accessories, different OS versions if testing an app on it. Performance: app launch time, multitasking. Reliability: does it survive a restart mid-update, does battery drain reporting stay accurate. Priority: for a phone specifically, I'd prioritize core call/text reliability and battery behavior first — those are the "must never break" fundamentals users notice immediately.
Clarify — email-based or SMS/OTP-based? Functional: request → email arrives → link works → new password set → can log in. Negative: non-existent email (no info leak), expired link, already-used link, mismatched password confirmation. Boundary: link expiry time edges. Security: old password invalidated immediately, no token reuse, rate-limit reset requests to prevent abuse. Data integrity: password is actually hashed and updated in the DB, old sessions ideally invalidated. Priority: I'd prioritize security and the negative/expiry cases hardest here, because a broken password-reset flow is a direct account-takeover risk, not just an inconvenience.
Functional: triggering event sends notification, tapping it opens the right screen (deep linking). Negative: notification permission denied by user — does the app fail gracefully or crash trying to send anyway? Boundary: extremely long notification text (truncation behavior). Reliability: app killed/backgrounded — does notification still arrive? Concurrency: many notifications fired simultaneously — batching/grouping behavior, or does the user get spammed? Priority: deliverability and correct deep-linking are the two things that actually matter to users; visual polish is secondary.
Functional: valid code applies correct discount. Negative: expired code, already-used single-use code, code for a different user/region, invalid/typo'd code. Boundary: minimum order value required to use the code, exactly at that value. Data integrity: does using the code correctly decrement its usage-count/limit in the DB, and prevent a second simultaneous use (concurrency) if it's single-use? Security: can the discount be manipulated client-side (e.g., editing the price in a request before the server recalculates)? Priority: preventing double-use and client-side price manipulation are the two categories I'd hit hardest — that's where real money is lost.
Functional: requests under the limit succeed normally. Negative: request exactly at/over the limit returns the correct status code (commonly 429 Too Many Requests), with a clear error body. Boundary: the exact request count at the limit boundary (BVA again — off-by-one is extremely common here, e.g., is it "100 requests allowed" or "101"?). Reliability: does the rate-limit counter reset correctly at the stated interval? Concurrency: many simultaneous requests near the limit — does the system correctly count them all, or does a race condition let a few extra through? Priority: the boundary/off-by-one check and the concurrency race-condition check are the two a strong candidate calls out unprompted — most candidates only think of the simple "does it block after N requests" case.
(Attempt the remaining 15 scenarios using the same framework — writing out your own answer, even briefly, is far more valuable than reading a 20th model answer.)
| Field | Purpose |
|---|---|
| Title | Short, specific, searchable — describes symptom + context |
| Description | Brief summary of the problem |
| Environment | Browser/OS/device/app version/build number |
| Preconditions | State required before the steps (e.g., logged in as X role) |
| Steps to Reproduce | Numbered, exact, minimal |
| Expected Result | What should have happened |
| Actual Result | What actually happened |
| Severity | Technical impact of the defect |
| Priority | Business urgency to fix |
| Evidence | Screenshot, screen recording, logs, HAR file |
| Reproducibility | Always / Sometimes (X out of Y attempts) / Once |
| Severity | Priority | |
|---|---|---|
| Definition | How badly the defect affects the system/functionality | How urgently it needs to be fixed, business-wise |
| Set by | QA (technical judgment) | Often product/business, sometimes jointly |
| Independent of each other? | Yes — they don't have to match | Yes |
A spelling mistake in the company name on the homepage is Low severity (nothing is functionally broken) but High priority (embarrassing, must be fixed before anyone important sees it, fix immediately). Conversely, a crash in a rarely-used admin report page is High severity (a crash!) but might be Low priority if almost no one uses that page and a workaround exists.
| Severity level | Meaning | Example |
|---|---|---|
| Critical/Blocker | System crash, data loss, complete feature failure, no workaround | Checkout page throws 500 error for all users |
| Major/High | Major function broken, but a workaround exists or it's isolated | Filter doesn't work, but users can still browse all products |
| Minor/Medium | Minor function affected, doesn't block core use | A secondary button is misaligned |
| Trivial/Low | Cosmetic, doesn't affect functionality | Typo in a tooltip |
❌ Bad:
Title: Login broken
It doesn't work when I try to log in. Please fix.
✅ Rewritten:
Login fails with "Internal Server Error" when password contains a special character (e.g., #)
Environment: Chrome 128, Windows 11, staging build v2.4.1
Preconditions: User account exists with password containing #
Steps to Reproduce:
1. Navigate to /login
2. Enter valid email qa.test@example.com
3. Enter password Pass#1234
4. Click "Login"
Expected Result: User logs in successfully and is redirected to the dashboard.
Actual Result: Page displays "500 Internal Server Error." No redirect occurs. Console shows a JS error (see attached screenshot).
Severity: Critical (blocks login entirely for any user whose password contains this character)
Priority: High
Reproducibility: Always (5/5 attempts)
Evidence: [screenshot attached], [console log attached]
Notice everything that changed: specificity, exact reproduction steps, precise actual vs expected, environment details, and a severity/priority judgment with reasoning.
stateDiagram-v2
[*] --> New
New --> Assigned
Assigned --> Open
Open --> Fixed
Fixed --> Retest
Retest --> Reopened: fix didn't work
Retest --> Closed: fix confirmed
Reopened --> Assigned
New --> Rejected: not a valid bug
New --> Duplicate: already reported
New --> WontFix: accepted, won't be fixed
Rejected --> [*]
Duplicate --> [*]
WontFix --> [*]
Closed --> [*]
| State | Meaning |
|---|---|
| New | Just reported, not yet triaged |
| Assigned | Assigned to a developer |
| Open | Developer acknowledges and is working on it |
| Fixed | Developer believes it's resolved, ready for QA to verify |
| Retest | QA is re-testing the fix |
| Reopened | Retest failed — the bug still exists (or wasn't fully fixed) |
| Closed | Retest passed — confirmed fixed |
| Rejected | Not actually a bug (e.g., working as intended, or tester error) |
| Duplicate | Already reported elsewhere |
| Won't Fix | Valid bug, but a business decision was made not to fix it (low value, too risky, deprecated feature) |
Rewrite this into a professional bug report: "The search doesn't show results sometimes, weird." Invent reasonable specific details (environment, exact steps, exact actual/expected) as you would need to in a real job.
| Term | Meaning |
|---|---|
| Project | Container for all work on a product |
| Issue | Generic term for any trackable item (bug, story, task) |
| Epic | A large body of work, broken into smaller stories |
| Story | A user-facing feature/requirement |
| Task | A piece of work, not necessarily user-facing |
| Bug | A defect issue type |
| Subtask | A smaller piece of a larger issue |
| Priority field | Business urgency (Highest/High/Medium/Low/Lowest) |
| Status | Workflow state (To Do / In Progress / In Review / Done, customizable) |
| Sprint | A time-boxed iteration issues are assigned to |
| Backlog | Unscheduled issues |
| Assignee | Who's responsible for the issue |
| Reporter | Who created the issue |
| Comment | Discussion thread on an issue |
| Attachment | Files (screenshots, logs) attached to an issue |
| Filter (JQL) | Jira Query Language — search issues, e.g. project = "MYAPP" AND status = "Open" AND assignee = currentUser() |
TestRail, Zephyr, and Xray are dedicated test-case management tools (often plugged into Jira) that organize test cases into suites/runs and track execution results and coverage separately from bug tracking. At entry level, know they exist and what problem they solve (organizing test cases, as distinct from Jira's job of tracking issues) — hands-on tool proficiency is picked up on the job in days, not something to pre-study deeply.
Using a free Jira Cloud trial (or the free tier) or even just on paper: create a mock Epic ("User Authentication"), one Story under it ("Password Reset"), and 2 linked Bug issues with full fields filled per Part 10's structure.
Dual-purpose: every backend dev job lives in a terminal too.
Many BD companies deploy on Linux servers; QA engineers often need to check application logs on a server, verify a file exists, check running processes, or use curl to hit an API directly. Written tests frequently include a "what does this command do" or "write a command to do X" section.
| Command | Purpose | Example |
|---|---|---|
pwd |
Print working directory | pwd |
ls |
List files | ls -la (show all, long format) |
cd |
Change directory | cd /var/log |
mkdir |
Make directory | mkdir test_data |
touch |
Create empty file | touch notes.txt |
cp |
Copy | cp file.txt backup.txt |
mv |
Move/rename | mv old.txt new.txt |
rm |
Remove | rm -r old_folder (recursive, be careful) |
cat |
Print full file content | cat app.log |
less |
View file page-by-page | less app.log |
head |
First N lines | head -n 20 app.log |
tail |
Last N lines (great for logs) | tail -f app.log (follow live) |
grep |
Search text | grep "ERROR" app.log |
find |
Find files | find . -name "*.log" |
chmod |
Change permissions | chmod +x script.sh |
ps |
List running processes | ps aux |
top |
Live process/resource monitor | top |
kill |
Terminate a process | kill -9 1234 |
curl |
Make HTTP requests from terminal | curl -X GET https://api.example.com/users |
wget |
Download a file | wget https://example.com/file.zip |
# Pipe: send output of one command as input to the next
cat app.log | grep "ERROR" | wc -l # count how many ERROR lines exist
# Redirection
echo "test run started" > log.txt # overwrite file
echo "test run finished" >> log.txt # append to file
grep "FAIL" results.txt > failures.txt # save matches to a new file
this exact cat | grep | wc -l pattern is how you'd quickly answer "how many errors happened in this log during the test run" without opening a huge file in an editor.
echo $HOME
export API_URL="https://staging.example.com"
echo $API_URL
Used constantly for storing config like API base URLs or credentials in test scripts without hardcoding them.
A backend error often isn't visible in the UI at all (silent failure) but is visible in the server logs. Practical QA move: tail -f the application log while manually reproducing a bug, to capture the exact stack trace/error for your bug report — this single technique dramatically improves your bug reports' usefulness to developers.
.txt files in the current directory and subdirectories.server.log.access.log contain the string "500".run_tests.sh execute permission."timeout" in app.log and save them to timeouts.txt.Test scripts, automation code, and test documentation increasingly live in version control alongside application code. Even manual QAs are commonly expected to check out a specific branch/build to test, and to know basic collaboration flow.
git init # start a new repo
git clone <url> # copy a remote repo locally
git status # see what's changed
git add file.txt # stage a file
git add . # stage everything
git commit -m "Add login test cases" # commit staged changes
git push # send commits to remote
git pull # fetch + merge latest remote changes
git branch # list branches
git branch feature/login-tests # create a branch
git checkout feature/login-tests # switch to a branch (older syntax)
git switch feature/login-tests # switch to a branch (modern syntax)
git merge feature/login-tests # merge a branch into current branch
git fetch # download remote changes without merging
A PR proposes merging one branch into another (commonly a feature branch into main), allowing review/discussion before the merge. As QA, you might open a PR for a new automated test script, or you might be asked to test the code in a PR branch before it merges.
.gitignoreA file listing patterns Git should never track (e.g., node_modules/, .env, __pycache__/, credentials, local config). Important because accidentally committing credentials/secrets is a real, common, embarrassing mistake — knowing to .gitignore sensitive files is a basic professional habit.
Create a free GitHub account (if you don't have one), create a new repo called qa-practice, clone it locally, create a branch, add a file with 3 sample test cases in it, commit, push, and open a PR merging it into main. This exact workflow is also your first step toward Part 23's portfolio projects.
Redesigned start-to-finish. This section now serves two goals at once: it's still the highest dual-purpose skill in the whole guide (QA data validation ↔ Django ORM later), and it's now also a real, advanced-level SQL track aimed at data-analyst roles like Commure/Augmedix Dhaka's RCM (Revenue Cycle Management) Data Analyst — window functions, CTEs, reconciliation logic, and query performance included. Don't rush the first half even if it feels easy; the advanced half assumes it's automatic.
SQL is just a structured way of asking a database questions — "show me," "count me," "which of these match." Everything from here to the end of this section is the same handful of questions asked in more and more sophisticated ways. Nothing here is a different subject; it's the same skill, going deeper.
QA data validation ("did this record actually save correctly?") uses the first half of this section constantly. An RCM data analyst role uses all of it, daily — that job is largely "write SQL against claims/payments/denial data to answer a business question, find a data problem, or build a report." So this section is ordered as one continuous ramp: basics → aggregation → joins → subqueries → CTEs → window functions → date/string/NULL handling → data cleaning & reconciliation → performance. Work through it in order; each part leans on the one before it.
Install PostgreSQL locally (preferred — it's what most modern data/backend teams in BD, including Django shops, actually run) or use a free sandbox (SQLZoo, DB Fiddle set to Postgres). For the first few basics sections we'll reuse a small generic schema; from 12.10 onward we switch to a small healthcare RCM schema — patients, providers, payers, claims, payments, denials — because that's the exact shape of data an RCM analyst works with every day, and because it gives every JOIN/subquery/window-function example a real business meaning instead of an abstract one.
-- generic schema (Parts 12.3 - 12.9)
CREATE TABLE users (
id INT PRIMARY KEY,
name VARCHAR(50),
email VARCHAR(100) UNIQUE,
country VARCHAR(50),
created_at DATE
);
CREATE TABLE orders (
id INT PRIMARY KEY,
user_id INT,
product_name VARCHAR(100),
amount DECIMAL(10,2),
status VARCHAR(20),
order_date DATE,
FOREIGN KEY (user_id) REFERENCES users(id)
);
-- RCM schema (Parts 12.10 onward) -- this is the one to really learn
CREATE TABLE payers (
payer_id INT PRIMARY KEY,
payer_name VARCHAR(100), -- 'Blue Cross', 'Medicare', 'Self-Pay', ...
payer_type VARCHAR(20) -- 'Commercial' / 'Government' / 'Self-Pay'
);
CREATE TABLE providers (
provider_id INT PRIMARY KEY,
provider_name VARCHAR(100),
specialty VARCHAR(50)
);
CREATE TABLE patients (
patient_id INT PRIMARY KEY,
full_name VARCHAR(100),
dob DATE,
payer_id INT REFERENCES payers(payer_id)
);
CREATE TABLE claims (
claim_id INT PRIMARY KEY,
patient_id INT REFERENCES patients(patient_id),
provider_id INT REFERENCES providers(provider_id),
payer_id INT REFERENCES payers(payer_id),
service_date DATE,
submitted_date DATE,
billed_amount DECIMAL(10,2),
claim_status VARCHAR(20) -- 'Submitted' / 'Paid' / 'Partially Paid' / 'Denied' / 'Pending'
);
CREATE TABLE claim_payments (
payment_id INT PRIMARY KEY,
claim_id INT REFERENCES claims(claim_id),
payment_date DATE,
paid_amount DECIMAL(10,2)
);
CREATE TABLE denials (
denial_id INT PRIMARY KEY,
claim_id INT REFERENCES claims(claim_id),
denial_code VARCHAR(10), -- e.g. 'CO-16', 'CO-97'
denial_reason VARCHAR(200),
denial_date DATE
);
erDiagram
PAYERS ||--o{ PATIENTS : covers
PAYERS ||--o{ CLAIMS : bills
PROVIDERS ||--o{ CLAIMS : renders
PATIENTS ||--o{ CLAIMS : has
CLAIMS ||--o{ CLAIM_PAYMENTS : receives
CLAIMS ||--o{ DENIALS : may_have
Revenue Cycle Management is the whole process of a healthcare provider getting paid: a patient is seen (encounter) → the visit is billed to an insurer as a claim → the payer (insurer) either pays it, partially pays it, or denies it with a reason code → unpaid/denied claims get worked, corrected, and resubmitted. An RCM data analyst's job is almost entirely: query this data to find where money is stuck, where claims are wrong, where denial patterns exist, and how fast (or slow) payers are paying. Every query pattern below maps directly onto that.
-- SELECT specific columns
SELECT name, email FROM users WHERE country = 'Bangladesh';
-- every column
SELECT * FROM users;
-- DISTINCT -- unique values only
SELECT DISTINCT country FROM users;
-- ORDER BY -- ASC is default, DESC reverses
SELECT name, created_at FROM users ORDER BY created_at DESC;
-- LIMIT -- cap the number of rows returned
SELECT * FROM users ORDER BY created_at DESC LIMIT 5;
SELECT = "show me these columns." FROM = "out of this table." WHERE = "but only rows matching this." ORDER BY = "sorted like this." LIMIT = "only give me this many." Read any query top to bottom in that order and it reads like a sentence.
-- LIKE (pattern matching: % = any characters, _ = exactly one character)
SELECT * FROM users WHERE email LIKE '%@gmail.com';
SELECT * FROM users WHERE name LIKE 'A%'; -- starts with A
SELECT * FROM users WHERE name ILIKE 'a%'; -- Postgres: case-insensitive LIKE
-- IN -- matches any value in a list
SELECT * FROM orders WHERE status IN ('Pending', 'Processing');
-- BETWEEN -- inclusive range
SELECT * FROM orders WHERE amount BETWEEN 500 AND 2000;
-- NULL handling -- this trips up almost everyone at first
SELECT * FROM users WHERE country IS NULL; -- correct
SELECT * FROM users WHERE country = NULL; -- WRONG -- always returns zero rows
SELECT * FROM users WHERE country IS NOT NULL; -- correct "has a value"
-- AND / OR / NOT, and why parentheses matter
SELECT * FROM orders WHERE status = 'Delivered' AND amount > 1000;
SELECT * FROM orders WHERE status = 'Pending' OR status = 'Processing' AND amount > 500;
-- ^ ambiguous to a human reader -- AND binds tighter than OR, so this actually means:
-- status = 'Pending' OR (status = 'Processing' AND amount > 500)
-- Always parenthesize explicitly once you mix AND/OR:
SELECT * FROM orders WHERE (status = 'Pending' OR status = 'Processing') AND amount > 500;
= NULL is a trap, explainedNULL means "unknown," not "empty." Any comparison against an unknown value is itself unknown — not true, not false. IS NULL / IS NOT NULL exist specifically because = and != can never detect NULL. This single rule explains a huge share of "why is my WHERE clause silently returning nothing" bugs, in QA and in real analyst work.
SELECT name,
CASE
WHEN country = 'Bangladesh' THEN 'Domestic'
ELSE 'International'
END AS region
FROM users;
-- multi-branch CASE, RCM-flavored
SELECT claim_id, billed_amount,
CASE
WHEN billed_amount >= 5000 THEN 'High Value'
WHEN billed_amount >= 1000 THEN 'Standard'
ELSE 'Low Value'
END AS claim_tier
FROM claims;
CASE is not a special feature — it's an expression, meaning it can appear anywhere a column can: in SELECT (shown above), inside WHERE, inside an aggregate function (12.9), even inside ORDER BY. That flexibility is exactly why it shows up constantly in both QA scripting and real analyst reporting.
INSERT INTO users (id, name, email, country, created_at)
VALUES (101, 'Rafi Ahmed', 'rafi@example.com', 'Bangladesh', '2026-08-01');
UPDATE orders SET status = 'Delivered' WHERE id = 45;
DELETE FROM orders WHERE status = 'Cancelled' AND order_date < '2025-01-01';
Always test UPDATE/DELETE with a SELECT using the exact same WHERE clause first, to see precisely which rows will be affected before you run the destructive version. In a real RCM analyst job, an un-scoped UPDATE claims SET claim_status = 'Paid' with a typo'd or missing WHERE clause is the kind of mistake that shows up in a postmortem. This habit alone prevents that class of incident.
SELECT COUNT(*) FROM orders;
SELECT SUM(amount) FROM orders WHERE status = 'Delivered';
SELECT AVG(amount) FROM orders;
SELECT MIN(order_date), MAX(order_date) FROM orders;
-- GROUP BY: one aggregate result per group
SELECT user_id, COUNT(*) AS order_count, SUM(amount) AS total_spent
FROM orders
GROUP BY user_id;
-- RCM version: total billed and claim count per payer
SELECT payer_id, COUNT(*) AS claim_count, SUM(billed_amount) AS total_billed
FROM claims
GROUP BY payer_id;
Every column in the SELECT list that is not wrapped in an aggregate function must appear in the GROUP BY clause. SELECT user_id, product_name, COUNT(*) FROM orders GROUP BY user_id is invalid in Postgres — product_name is neither aggregated nor grouped, so the database doesn't know which row's product_name to show for a group that spans many rows.
-- HAVING filters AFTER grouping/aggregation -- WHERE can't do this
SELECT user_id, SUM(amount) AS total_spent
FROM orders
GROUP BY user_id
HAVING SUM(amount) > 5000;
-- grouping by more than one column (a "complex" GROUP BY)
SELECT payer_id, claim_status, COUNT(*) AS claim_count
FROM claims
GROUP BY payer_id, claim_status
ORDER BY payer_id, claim_status;
-- HAVING combined with a real business question:
-- which payers have more than 10 denied claims?
SELECT payer_id, COUNT(*) AS denied_count
FROM claims
WHERE claim_status = 'Denied'
GROUP BY payer_id
HAVING COUNT(*) > 10;
WHERE filters individual rows before grouping happens. HAVING filters entire groups after aggregation. You cannot write WHERE SUM(amount) > 5000 — at the point WHERE runs, no sum has been computed yet. This is one of the most commonly asked "explain the difference" questions in both QA and analyst interviews.
Putting a CASE expression inside an aggregate function lets you count or sum only the rows that match a condition, instead of running a separate query per condition. It's how you build a single "pivoted" summary row instead of many narrow ones — exactly what a report or dashboard query needs to do.
-- one row per payer, with paid/denied/pending counts side by side
SELECT
payer_id,
COUNT(*) AS total_claims,
COUNT(CASE WHEN claim_status = 'Paid' THEN 1 END) AS paid_count,
COUNT(CASE WHEN claim_status = 'Denied' THEN 1 END) AS denied_count,
COUNT(CASE WHEN claim_status = 'Pending' THEN 1 END) AS pending_count,
SUM(CASE WHEN claim_status = 'Paid' THEN billed_amount ELSE 0 END) AS paid_amount
FROM claims
GROUP BY payer_id;
Compare this to running four separate WHERE claim_status = 'X' GROUP BY payer_id queries and stitching the results together by hand — conditional aggregation does it in one pass, one query, one result set. This exact pattern is one of the most commonly asked "write me a query" questions in data-analyst screens, because it directly tests whether you can build a report, not just filter a table.
From here on we use the RCM schema (12.2).
-- INNER JOIN: only rows with a match in both tables
SELECT c.claim_id, p.full_name, c.billed_amount
FROM claims c
INNER JOIN patients p ON c.patient_id = p.patient_id;
-- LEFT JOIN: all rows from claims, matched payment info if it exists (else NULL)
SELECT c.claim_id, c.billed_amount, cp.paid_amount
FROM claims c
LEFT JOIN claim_payments cp ON c.claim_id = cp.claim_id;
-- find claims that were never paid at all (classic RCM + QA task)
SELECT c.claim_id, c.billed_amount
FROM claims c
LEFT JOIN claim_payments cp ON c.claim_id = cp.claim_id
WHERE cp.payment_id IS NULL;
-- RIGHT JOIN: mirror of LEFT JOIN -- rarely used in practice; most people
-- just flip table order and write LEFT JOIN instead, for readability
SELECT c.claim_id, cp.paid_amount
FROM claim_payments cp
RIGHT JOIN claims c ON c.claim_id = cp.claim_id;
-- multi-table join: claim + patient + provider + payer, all at once
SELECT
c.claim_id, p.full_name AS patient, pr.provider_name,
py.payer_name, c.billed_amount, c.claim_status
FROM claims c
JOIN patients p ON c.patient_id = p.patient_id
JOIN providers pr ON c.provider_id = pr.provider_id
JOIN payers py ON c.payer_id = py.payer_id;
"Find all claims that were never paid" is exactly the LEFT JOIN + WHERE ... IS NULL pattern above. Memorize this pattern — it's asked constantly in both QA and analyst interviews because it directly mirrors a real task: finding data-integrity gaps (QA framing) or unpaid revenue (RCM framing). Same query, two job titles.
-- classic self join: employees and their managers, same table
SELECT e.name AS employee, m.name AS manager
FROM employees e
JOIN employees m ON e.manager_id = m.id;
-- RCM-flavored self join: find pairs of claims for the same patient
-- submitted on the same day (possible duplicate submission)
SELECT c1.claim_id AS claim_a, c2.claim_id AS claim_b, c1.patient_id, c1.submitted_date
FROM claims c1
JOIN claims c2
ON c1.patient_id = c2.patient_id
AND c1.submitted_date = c2.submitted_date
AND c1.claim_id < c2.claim_id; -- avoids matching a row with itself, and avoids duplicate mirror pairs
A self join is just joining a table to a second copy of itself so you can compare rows within the same table to each other — "does any other row in this same table relate to this row." The c1.claim_id < c2.claim_id trick is how you stop it from matching a claim to itself, and from returning every pair twice (once each direction).
-- subquery in WHERE
SELECT full_name FROM patients
WHERE patient_id IN (SELECT patient_id FROM claims WHERE billed_amount > 5000);
-- subquery in FROM (a "derived table")
SELECT payer_id, AVG(claim_total) AS avg_claim_total
FROM (
SELECT payer_id, SUM(billed_amount) AS claim_total
FROM claims
GROUP BY payer_id, patient_id
) sub
GROUP BY payer_id;
-- scalar subquery in SELECT
SELECT claim_id, billed_amount,
(SELECT AVG(billed_amount) FROM claims) AS overall_avg
FROM claims;
A normal subquery runs once, on its own, and hands back a fixed result. A correlated subquery reruns once per row of the outer query, because it references a column from the outer row — think of it as a mini-question asked separately for every single row, instead of one question asked once.
-- for each claim, is its billed_amount above the average for THAT SAME payer?
-- (note: claims.payer_id below refers to the OUTER query's current row --
-- that's what makes this correlated, not a plain subquery)
SELECT c1.claim_id, c1.payer_id, c1.billed_amount
FROM claims c1
WHERE c1.billed_amount > (
SELECT AVG(c2.billed_amount)
FROM claims c2
WHERE c2.payer_id = c1.payer_id
);
Being able to say out loud "this is correlated because the inner query references the outer query's current row, so conceptually it re-runs per outer row" is a strong signal in a data-analyst screen — it shows you understand why the query works, not just that it happens to return the right answer. It also foreshadows 12.29-12.31 (performance): a correlated subquery over a huge table can be slow precisely because of that per-row re-execution, which is exactly the kind of thing EXPLAIN will show you.
-- EXISTS -- often faster than IN for large tables, because it can stop
-- at the first match instead of building a full list to compare against
SELECT p.full_name FROM patients p
WHERE EXISTS (SELECT 1 FROM claims c WHERE c.patient_id = p.patient_id AND c.billed_amount > 5000);
-- NOT EXISTS -- the professional-standard way to write "find missing" queries,
-- often preferred over LEFT JOIN ... IS NULL for readability at scale
SELECT c.claim_id FROM claims c
WHERE NOT EXISTS (SELECT 1 FROM claim_payments cp WHERE cp.claim_id = c.claim_id);
UNION and UNION ALL are worth knowing too: UNION combines two result sets and removes duplicates; UNION ALL combines them and keeps everything, which is also faster, since Postgres skips the duplicate-removal pass.
A CTE (Common Table Expression) is a named, temporary result set you define at the top of a query with WITH name AS (...), then use later in that same query exactly like a real table. It exists purely to make a complicated query readable — break it into named, logical steps instead of nesting nine subqueries inside each other.
WITH denied_claims AS (
SELECT claim_id, payer_id, billed_amount
FROM claims
WHERE claim_status = 'Denied'
)
SELECT payer_id, COUNT(*) AS denied_count, SUM(billed_amount) AS denied_value
FROM denied_claims
GROUP BY payer_id
ORDER BY denied_value DESC;
WITH payer_totals AS (
SELECT payer_id, SUM(billed_amount) AS total_billed
FROM claims
GROUP BY payer_id
),
payer_paid AS (
SELECT c.payer_id, SUM(cp.paid_amount) AS total_paid
FROM claims c
JOIN claim_payments cp ON c.claim_id = cp.claim_id
GROUP BY c.payer_id
)
SELECT
pt.payer_id,
pt.total_billed,
COALESCE(pp.total_paid, 0) AS total_paid,
pt.total_billed - COALESCE(pp.total_paid, 0) AS outstanding
FROM payer_totals pt
LEFT JOIN payer_paid pp ON pt.payer_id = pp.payer_id
ORDER BY outstanding DESC;
This is a realistic shape for a real RCM report: each CTE computes one clean, named intermediate answer ("total billed per payer," "total paid per payer"), and the final SELECT just combines them. Chaining CTEs like this — instead of one giant nested query — is exactly how a readable analyst query is structured, and it's a strong habit to demonstrate in a take-home or live SQL screen.
A recursive CTE is a query that refers to itself, so it can walk through data that has a "repeat this logic on the result of the last step" shape — a chain of dates, an org chart, a category that contains sub-categories. You almost certainly won't be asked to write one from memory at entry level, but recognizing the shape and being able to read one is a real differentiator.
-- generate a series of dates (useful for building a complete aging report
-- that doesn't silently skip days with zero claims)
WITH RECURSIVE date_series AS (
SELECT DATE '2026-01-01' AS d
UNION ALL
SELECT d + 1 FROM date_series WHERE d + 1 <= DATE '2026-01-31'
)
SELECT d FROM date_series;
-- (in real Postgres work you'd usually reach for generate_series() instead --
-- generate_series(DATE '2026-01-01', DATE '2026-01-31', INTERVAL '1 day') --
-- but understanding the recursive-CTE version is what explains WHY that
-- built-in function exists and what it's doing under the hood)
Structurally every recursive CTE has the same two halves: an anchor (the starting row(s), before UNION ALL) and a recursive term (the part after UNION ALL that refers back to the CTE's own name) that keeps running until it produces no more new rows.
GROUP BY collapses many rows into one row per group — you lose the individual rows. A window function calculates something across a group of related rows (a "window") but keeps every original row visible. That's the entire idea: aggregate-like power, without collapsing the detail.
-- every window function follows this shape:
-- FUNCTION() OVER (PARTITION BY ... ORDER BY ...)
-- PARTITION BY = "reset/group by this" (like GROUP BY, but doesn't collapse rows)
-- ORDER BY inside OVER() = the order the window function should process rows in
SELECT
claim_id, payer_id, billed_amount,
AVG(billed_amount) OVER (PARTITION BY payer_id) AS payer_avg_amount
FROM claims;
-- every claim row still shows individually, but now also shows its payer's average
-- alongside it -- impossible with plain GROUP BY without a self-join
Window functions are what separates "can write basic reporting SQL" from "can write real analyst SQL." RCM-style questions like "rank each patient's claims by amount," "show each claim next to the payer's running total," or "find the days between a claim and that same patient's previous claim" are all window-function questions. Expect them in a Commure/Augmedix-style screen.
SELECT
claim_id, payer_id, billed_amount,
ROW_NUMBER() OVER (PARTITION BY payer_id ORDER BY billed_amount DESC) AS rn,
RANK() OVER (PARTITION BY payer_id ORDER BY billed_amount DESC) AS rnk,
DENSE_RANK() OVER (PARTITION BY payer_id ORDER BY billed_amount DESC) AS drnk
FROM claims;
-- classic use: the single highest-billed claim per payer
WITH ranked AS (
SELECT claim_id, payer_id, billed_amount,
ROW_NUMBER() OVER (PARTITION BY payer_id ORDER BY billed_amount DESC) AS rn
FROM claims
)
SELECT * FROM ranked WHERE rn = 1;
| Function | Ties (same value) | Gaps after a tie |
|---|---|---|
ROW_NUMBER() | Breaks ties arbitrarily — always gives 1,2,3,4... | No gaps, ever |
RANK() | Tied rows get the same rank | Skips the next rank(s) — e.g. 1,2,2,4 |
DENSE_RANK() | Tied rows get the same rank | No gap — e.g. 1,2,2,3 |
"What's the difference between RANK and DENSE_RANK" is asked constantly precisely because the table above is easy to memorize but easy to blank on live. The trick to never forgetting it: RANK "leaves a gap the size of the tie," DENSE_RANK never leaves gaps. ROW_NUMBER never even looks at ties — it just numbers 1..n regardless.
-- for each claim, show the previous claim's submitted_date for that same patient
SELECT
claim_id, patient_id, submitted_date,
LAG(submitted_date) OVER (PARTITION BY patient_id ORDER BY submitted_date) AS prev_submitted_date,
submitted_date - LAG(submitted_date) OVER (PARTITION BY patient_id ORDER BY submitted_date) AS days_since_last_claim
FROM claims;
-- LEAD works identically but looks FORWARD instead of backward
SELECT
claim_id, patient_id, submitted_date,
LEAD(submitted_date) OVER (PARTITION BY patient_id ORDER BY submitted_date) AS next_submitted_date
FROM claims;
LAG = "what was the value in the previous row (within this patient's group)?" LEAD = "what's the value in the next row?" It's how you compare a row to its neighbor without a self-join — genuinely one of the most useful patterns for anything involving a sequence of events over time, which is most of what RCM data is.
-- running (cumulative) total of payments received, ordered by date
SELECT
payment_date, paid_amount,
SUM(paid_amount) OVER (ORDER BY payment_date) AS running_total
FROM claim_payments
ORDER BY payment_date;
-- 7-row moving average (frame clause -- advanced but good to recognize)
SELECT
payment_date, paid_amount,
AVG(paid_amount) OVER (
ORDER BY payment_date
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
) AS moving_avg_7
FROM claim_payments
ORDER BY payment_date;
This is exactly the query shape behind "collections trend over time" or "revenue running total" charts — a dashboard's line chart is almost always a window function like this, one row per data point, running underneath it.
-- date arithmetic
SELECT claim_id, submitted_date, submitted_date + 30 AS due_in_30_days FROM claims;
-- difference between two dates (Postgres returns an integer number of days)
SELECT claim_id, service_date, submitted_date, submitted_date - service_date AS days_to_submit
FROM claims;
-- EXTRACT -- pull out a part of a date
SELECT claim_id, EXTRACT(YEAR FROM submitted_date) AS yr, EXTRACT(MONTH FROM submitted_date) AS mo
FROM claims;
-- DATE_TRUNC -- round a date DOWN to a unit -- the standard way to group by month/week
SELECT DATE_TRUNC('month', submitted_date) AS claim_month, COUNT(*) AS claim_count, SUM(billed_amount) AS total_billed
FROM claims
GROUP BY DATE_TRUNC('month', submitted_date)
ORDER BY claim_month;
-- the single most important RCM date query: days-to-payment, and aging buckets
SELECT
c.claim_id,
c.submitted_date,
cp.payment_date,
cp.payment_date - c.submitted_date AS days_to_payment,
CASE
WHEN cp.payment_id IS NULL AND CURRENT_DATE - c.submitted_date <= 30 THEN '0-30 days'
WHEN cp.payment_id IS NULL AND CURRENT_DATE - c.submitted_date <= 60 THEN '31-60 days'
WHEN cp.payment_id IS NULL AND CURRENT_DATE - c.submitted_date <= 90 THEN '61-90 days'
WHEN cp.payment_id IS NULL THEN '90+ days'
ELSE 'Paid'
END AS aging_bucket
FROM claims c
LEFT JOIN claim_payments cp ON c.claim_id = cp.claim_id;
"Accounts Receivable aging" (how long unpaid claims have been sitting, bucketed into ranges like 0-30/31-60/61-90/90+ days) is one of the single most common reports in any RCM role, because the older an unpaid claim gets, the less likely it is to ever be collected. Being able to build this query from scratch — CASE + date arithmetic + a LEFT JOIN for "unpaid" — is close to a guaranteed interview or take-home question for this exact role.
SELECT UPPER(payer_name), LOWER(payer_name) FROM payers;
-- concatenation
SELECT full_name || ' (' || patient_id || ')' AS patient_label FROM patients;
SELECT CONCAT(full_name, ' (', patient_id, ')') AS patient_label FROM patients;
-- substring / trimming / length
SELECT SUBSTRING(denial_code FROM 1 FOR 2) AS code_prefix FROM denials; -- e.g. 'CO' from 'CO-16'
SELECT TRIM(payer_name) FROM payers; -- strips leading/trailing whitespace
SELECT LENGTH(denial_reason) FROM denials;
-- REPLACE -- fixing/normalizing messy text data (very common in data cleaning)
SELECT REPLACE(payer_name, 'Ins.', 'Insurance') FROM payers;
-- pattern-based extraction with regex (Postgres)
SELECT denial_code FROM denials WHERE denial_code ~ '^CO-'; -- ~ = regex match
-- COALESCE -- return the first non-NULL value in a list; the standard way
-- to supply a default instead of a NULL
SELECT claim_id, COALESCE(cp.paid_amount, 0) AS paid_amount
FROM claims c
LEFT JOIN claim_payments cp ON c.claim_id = cp.claim_id;
-- NULLIF -- return NULL if two values are equal (useful to avoid divide-by-zero)
SELECT payer_id, total_billed / NULLIF(claim_count, 0) AS avg_claim_value
FROM (SELECT payer_id, SUM(billed_amount) AS total_billed, COUNT(*) AS claim_count FROM claims GROUP BY payer_id) t;
-- IS DISTINCT FROM -- a NULL-safe equality check (treats NULL = NULL as true)
SELECT * FROM claims WHERE claim_status IS DISTINCT FROM 'Paid';
COALESCE(x, 0) means "use x, but if x is missing, use 0 instead." It's the single most-used NULL-handling function in real reporting SQL, because unmatched LEFT JOINs produce NULLs constantly (12.10) and you almost always want a clean default (0, 'Unknown', etc.) instead of a blank in your final report.
-- duplicate detection pattern -- memorize this, asked constantly
SELECT patient_id, service_date, provider_id, COUNT(*) AS cnt
FROM claims
GROUP BY patient_id, service_date, provider_id
HAVING COUNT(*) > 1;
-- reads as: "same patient, same day, same provider, more than once" --
-- a strong signal of an accidental duplicate claim submission
-- data validation: are billed_amount values ever impossible (negative, zero, absurd)?
SELECT * FROM claims WHERE billed_amount <= 0 OR billed_amount > 100000;
-- data validation: does every claim reference a patient/provider/payer that actually exists?
-- (referential-integrity check -- should return zero rows in a healthy database)
SELECT c.claim_id FROM claims c
LEFT JOIN patients p ON c.patient_id = p.patient_id
WHERE p.patient_id IS NULL;
-- data cleaning: normalize inconsistent status text before analysis
SELECT claim_id, INITCAP(TRIM(claim_status)) AS clean_status FROM claims;
Reconciliation just means "do two numbers that are supposed to match, actually match?" In RCM: does the sum of what was billed equal the sum of what was recorded as paid plus what's still outstanding? If not, money is unaccounted for — and finding exactly which claims caused the mismatch is the actual job.
-- does billed_amount reconcile against the sum of its payments?
SELECT
c.claim_id,
c.billed_amount,
COALESCE(SUM(cp.paid_amount), 0) AS total_paid,
c.billed_amount - COALESCE(SUM(cp.paid_amount), 0) AS variance
FROM claims c
LEFT JOIN claim_payments cp ON c.claim_id = cp.claim_id
GROUP BY c.claim_id, c.billed_amount
HAVING c.billed_amount - COALESCE(SUM(cp.paid_amount), 0) <> 0;
-- find "orphaned" payments -- a payment referencing a claim_id that
-- doesn't exist in claims at all (a real, damaging data-integrity bug)
SELECT cp.* FROM claim_payments cp
LEFT JOIN claims c ON cp.claim_id = c.claim_id
WHERE c.claim_id IS NULL;
-- claims marked 'Paid' that actually have NO payment row -- a status/data mismatch
SELECT c.claim_id FROM claims c
LEFT JOIN claim_payments cp ON c.claim_id = cp.claim_id
WHERE c.claim_status = 'Paid' AND cp.payment_id IS NULL;
-- claims billed far above the norm for their payer (simple outlier check)
WITH payer_stats AS (
SELECT payer_id, AVG(billed_amount) AS avg_amt, STDDEV(billed_amount) AS stddev_amt
FROM claims
GROUP BY payer_id
)
SELECT c.claim_id, c.payer_id, c.billed_amount, ps.avg_amt
FROM claims c
JOIN payer_stats ps ON c.payer_id = ps.payer_id
WHERE c.billed_amount > ps.avg_amt + 3 * ps.stddev_amt; -- more than 3 standard deviations out
-- a payer whose denial rate suddenly looks wrong this month vs. last month
-- (this is exactly a LAG comparison -- 12.20 -- applied to a monthly aggregate)
WITH monthly_denials AS (
SELECT payer_id, DATE_TRUNC('month', denial_date) AS mo, COUNT(*) AS denied_count
FROM denials d JOIN claims c ON d.claim_id = c.claim_id
GROUP BY payer_id, DATE_TRUNC('month', denial_date)
)
SELECT payer_id, mo, denied_count,
LAG(denied_count) OVER (PARTITION BY payer_id ORDER BY mo) AS prev_month_count
FROM monthly_denials;
You don't need to be a statistician for this role. "More than N standard deviations from the mean" and "compare this period to the previous period with LAG" cover the large majority of anomaly-detection questions an entry-level RCM analyst is realistically asked to build.
A real analyst deliverable rarely uses just one technique — it chains several. Here's a full "denial rate and revenue impact by payer" report, combining a CTE, conditional aggregation, and a window function in one query:
WITH claim_summary AS (
SELECT
c.payer_id,
py.payer_name,
COUNT(*) AS total_claims,
COUNT(CASE WHEN c.claim_status = 'Denied' THEN 1 END) AS denied_claims,
SUM(CASE WHEN c.claim_status = 'Denied' THEN c.billed_amount ELSE 0 END) AS denied_revenue,
SUM(c.billed_amount) AS total_billed
FROM claims c
JOIN payers py ON c.payer_id = py.payer_id
GROUP BY c.payer_id, py.payer_name
)
SELECT
payer_name,
total_claims,
denied_claims,
ROUND(100.0 * denied_claims / NULLIF(total_claims, 0), 1) AS denial_rate_pct,
denied_revenue,
RANK() OVER (ORDER BY denied_revenue DESC) AS revenue_impact_rank
FROM claim_summary
ORDER BY revenue_impact_rank;
Notice the shape: a CTE builds one clean per-payer summary; the outer query turns that into percentages and a rank. This "CTE that summarizes, then an outer query that presents" pattern is worth internalizing — it's how the overwhelming majority of real analyst SQL is actually structured, and it's a strong signal to write it this way (instead of one giant nested query) in any live SQL exercise.
An index is like the index at the back of a textbook — instead of reading every page to find a topic, you jump straight to the right page. Without an index on a column, the database has to scan every single row to find matches on that column; with one, it can jump almost straight there.
CREATE INDEX idx_claims_patient_id ON claims(patient_id);
CREATE INDEX idx_claims_payer_status ON claims(payer_id, claim_status); -- composite index
| Good candidate for an index | Usually not worth indexing |
|---|---|
Columns used constantly in WHERE/JOIN (e.g. patient_id, foreign keys) | Columns almost never filtered/joined on |
| Columns used in ORDER BY on large result sets | Small tables (a full scan is already fast) |
| High-cardinality columns (many distinct values) | Low-cardinality columns (e.g. a boolean flag) — an index barely helps |
The tradeoff: indexes speed up reads but slow down writes slightly (every INSERT/UPDATE also has to update the index), and they take disk space. As an analyst you'll rarely be the one deciding what to index, but you're expected to recognize why a query is slow and be able to say "this needs an index on X" when asked.
EXPLAIN ANALYZE
SELECT c.claim_id, p.full_name
FROM claims c
JOIN patients p ON c.patient_id = p.patient_id
WHERE c.claim_status = 'Denied';
You don't need to master every line of a query plan. Look for: Seq Scan (sequential scan — reading every row; fine on a small table, a red flag on a huge one if there's a WHERE/JOIN column that could be indexed instead) vs. Index Scan (using an index — the thing you generally want on large tables); the estimated vs. actual row counts (a huge mismatch means the database's statistics are stale, which can cause it to pick a bad plan); and the total execution time at the bottom. Being able to say "I ran EXPLAIN ANALYZE and saw a sequential scan on a 2-million-row table, which is why this took 8 seconds" is exactly the kind of answer that separates a candidate who's only ever written correct SQL from one who's also written fast SQL.
The "N+1 problem" is running one query to get a list, then a separate query per row of that list to get related data — 1 query becomes 1 + N queries. It's usually an application/ORM mistake (fetch 500 claims, then loop and run a separate "get payments for this claim" query 500 times) rather than something you'd write by hand in raw SQL, but recognizing it is valuable because the fix is almost always "do it in one JOIN or one window function instead" — exactly the tools from this whole section.
SELECT * in real reporting queries; it pulls unnecessary data across the network and can prevent certain index-only optimizations.WHERE DATE(submitted_date) = '2026-01-01' can prevent the database from using an index on submitted_date; WHERE submitted_date >= '2026-01-01' AND submitted_date < '2026-01-02' usually can.| Concept | What you actually check |
|---|---|
| CRUD validation | Does Create/Read/Update/Delete via the UI/API correctly reflect in the DB? |
| Data validation | Are the values stored correct, correctly typed, correctly formatted? |
| Referential integrity | Are foreign keys always valid — no orphaned records? (12.25/12.26) |
| Data consistency | Does related data agree everywhere (e.g. "total claims" shown in a UI matches actual COUNT in DB)? |
| Migration validation | After a data/schema migration, does row count and sampled data match pre-migration? |
| Duplicate detection | The GROUP BY ... HAVING COUNT(*) > 1 pattern (12.25) |
| NULL validation | Are required fields actually enforced as NOT NULL, or can bad data sneak in? |
Notice that every row of this table is a direct application of a technique already taught above — database testing for QA isn't a separate skill from RCM analyst SQL, it's a subset use-case of the exact same queries.
Problems 1-20 use the generic users/orders schema (12.2). Problems 21-55 use the RCM schema (patients, providers, payers, claims, claim_payments, denials) — set that schema up for real and run these against it; that combination is the closest realistic simulation of an actual Commure/Augmedix take-home you can give yourself.
Foundations (1–20)
users.name and email from users.'Bangladesh'.amount greater than 1000.status values from orders.created_at ascending.'gmail'.'Pending' or 'Processing'.country is NULL.users table, if any.Joins, subqueries, CTEs — RCM schema (21–35)
patient_id that doesn't exist in patients (should return zero rows in a healthy DB).Window functions & date/time — RCM schema (36–48)
Full analyst-style problems (49–55)
submitted_date earlier than their service_date (impossible in reality). Write a query to find and count them.EXPLAIN ANALYZE to investigate why problem 54's query is slow on a 5-million-row claims table, and what you'd look for in the output.WHERE and HAVING?PARTITION BY do, and how is it different from GROUP BY?COALESCE and NULLIF, and give a real use case for each.EXPLAIN ANALYZE output if a query was running slowly?This is a genuine differentiator in BD junior QA hiring — many candidates only know manual UI testing; API testing skill puts you ahead.
APIs are faster to test (no UI rendering), more stable (UI changes don't break your tests), and let you test backend logic and edge cases the UI might not even expose (e.g., sending malformed data the UI form would normally prevent you from typing). API testing is also how you verify the backend is correct independent of whether the frontend correctly displays it — isolating where a bug actually lives.
REST APIs organize functionality around resources (nouns, e.g. /users, /orders) and use HTTP methods (verbs) to say what to do:
| Method | Purpose | Idempotent? | Example |
|---|---|---|---|
| GET | Retrieve data | Yes | GET /users/5 |
| POST | Create new data | No | POST /users |
| PUT | Replace an entire resource | Yes | PUT /users/5 |
| PATCH | Partially update a resource | No (generally) | PATCH /users/5 |
| DELETE | Remove a resource | Yes | DELETE /users/5 |
Idempotent means calling it multiple times has the same effect as calling it once (GET-ing the same resource repeatedly doesn't change anything; PUT-ing the exact same full replacement repeatedly leaves the same end state). POST is NOT idempotent — calling it twice typically creates two resources. This idempotency distinction is a real interview question at API-testing-aware companies.
| Part | Purpose | Example |
|---|---|---|
| Headers | Metadata about the request | Content-Type: application/json, Authorization: Bearer <token> |
| Query parameters | Key-value pairs in the URL, usually for filtering/pagination | GET /orders?status=pending&page=2 |
| Path parameters | Part of the URL path identifying a specific resource | GET /orders/{id} → /orders/42 |
| Request body | The actual data being sent (POST/PUT/PATCH) | {"name": "Rafi", "email": "rafi@x.com"} |
A full API test checks, at minimum:
Content-Type: application/json present and correct.POST /api/users — creates a user, requires name (string, 2-50 chars) and email (valid format, unique).
| Test | Type | Expected |
|---|---|---|
| Valid name + valid unique email | Positive | 201 Created, response body has new user with correct id |
Missing name field |
Negative | 400 Bad Request, error message identifies the missing field |
email already exists |
Negative | 409 Conflict (or 400, depending on API design — this is exactly the kind of thing you verify against the actual spec, not assume) |
Invalid email format ("notanemail") |
Negative | 400 Bad Request |
name exactly 2 characters |
Boundary | 201 Created (min boundary, valid) |
name 1 character |
Boundary | 400 Bad Request (just below min) |
name exactly 50 characters |
Boundary | 201 Created (max boundary, valid) |
name 51 characters |
Boundary | 400 Bad Request (just above max) |
No Authorization header (if auth required) |
Security | 401 Unauthorized |
| Extremely large request body (e.g., 10MB payload) | Negative/stress | Should reject gracefully, not crash the server |
SQL-injection-style string in name (' OR '1'='1) |
Security | Should be safely stored/rejected as a literal string, never executed as SQL |
Notice: this is literally the EP/BVA techniques from Part 7 applied directly to an API instead of a UI field — same thinking, different surface.
Most real APIs require auth on protected endpoints — commonly a Bearer token in the Authorization header (Authorization: Bearer <token>), obtained via a login/token endpoint first. Test both: valid token works, and — importantly — an expired/invalid/missing token is correctly rejected (401), and a valid token for User A cannot access User B's private data (403 or 404 depending on design — this is an authorization check, not authentication, and it's one of the most commonly missed real-world bug classes).
POST /login endpoint.| Concept | Purpose |
|---|---|
| Request | A single API call configuration (method, URL, headers, body) |
| Collection | A folder of related requests |
| Environment | A named set of variables (e.g., "Staging" vs "Production") with different values |
| Variable | A placeholder like {{base_url}} reused across requests |
| Tests tab | JavaScript assertions run after the response arrives |
| Collection Runner | Runs an entire collection's requests in sequence, useful for a quick regression pass |
// Status code check
pm.test("Status code is 200", function () {
pm.response.to.have.status(200);
});
// Response body field check
pm.test("Response has correct user id", function () {
const jsonData = pm.response.json();
pm.expect(jsonData.id).to.eql(5);
});
// Response time check (basic performance awareness)
pm.test("Response time is under 1000ms", function () {
pm.expect(pm.response.responseTime).to.be.below(1000);
});
// Save a value from this response for use in a LATER request (chaining)
pm.test("Save auth token", function () {
const jsonData = pm.response.json();
pm.environment.set("auth_token", jsonData.token);
});
A login request saves auth_token into an environment variable (as above); the next request (e.g., "Get My Orders") uses {{auth_token}} in its Authorization header. This models real user flows and is a core Postman skill.
Using a free public test API — e.g., ReqRes or JSONPlaceholder — build a Postman collection that:
404), POST with missing required fields, invalid data types.{{base_url}} variable..json collection file) — this becomes part of your Portfolio Project 2 in Part 23.This section is written slightly deeper than a pure-QA course would go, deliberately — this is the same foundation Django needs. Every extra hour here is time you won't have to re-spend in your Django phase.
Python is the most common scripting/automation language in BD QA job postings (alongside Java), reads close to plain English, and — not incidentally — is also what Django is written in. Learning it once, well, serves both purposes.
name = "Shahed" # str
age = 26 # int
price = 199.99 # float
is_active = True # bool
nothing = None # NoneType
s = "Hello, QA World"
print(s.lower()) # hello, qa world
print(s.upper()) # HELLO, QA WORLD
print(s.strip()) # remove leading/trailing whitespace
print(s.replace("QA", "Testing"))
print(s.split(",")) # ['Hello', ' QA World']
print(len(s)) # 16
print(s[0:5]) # 'Hello' (slicing)
print(f"Name: {name}, Age: {age}") # f-strings — use these, not old-style formatting
# List — ordered, mutable
fruits = ["apple", "banana", "cherry"]
fruits.append("date")
fruits.remove("banana")
print(fruits[0]) # 'apple'
print(len(fruits))
# Tuple — ordered, immutable
point = (10, 20)
# Set — unordered, unique values (great for "find duplicates" style logic)
unique_ids = {1, 2, 2, 3} # {1, 2, 3}
# Dictionary — key-value pairs (this is what JSON becomes in Python)
user = {"name": "Shahed", "age": 26, "roles": ["tester"]}
print(user["name"])
user["email"] = "shahed@example.com" # add a key
print(user.get("phone", "N/A")) # safe access with default
score = 72
if score >= 90:
grade = "A"
elif score >= 70:
grade = "B"
else:
grade = "C"
for fruit in fruits:
print(fruit)
for i in range(5): # 0,1,2,3,4
print(i)
count = 0
while count < 3:
print(count)
count += 1
def calculate_discount(price, percent):
"""Returns the discounted price."""
if percent < 0 or percent > 100:
raise ValueError("Percent must be between 0 and 100")
return price - (price * percent / 100)
print(calculate_discount(1000, 10)) # 900.0
try:
result = 10 / 0
except ZeroDivisionError as e:
print(f"Error occurred: {e}")
finally:
print("This always runs")
Exception handling matters directly for QA: your test scripts need to handle unexpected API failures/timeouts gracefully instead of crashing the whole test run.
# Writing/reading a text file
with open("results.txt", "w") as f:
f.write("Test run passed: 45/50\n")
with open("results.txt", "r") as f:
print(f.read())
# JSON — critical for API testing scripts
import json
data = {"id": 5, "name": "Shahed", "active": True}
json_string = json.dumps(data) # Python dict -> JSON string
parsed_back = json.loads(json_string) # JSON string -> Python dict
print(parsed_back["name"])
class TestCase:
def __init__(self, title, priority):
self.title = title
self.priority = priority
self.status = "Not Run"
def mark_passed(self):
self.status = "Passed"
def __str__(self):
return f"{self.title} [{self.priority}] - {self.status}"
tc = TestCase("Verify login with valid credentials", "High")
tc.mark_passed()
print(tc) # Verify login with valid credentials [High] - Passed
class SmokeTestCase(TestCase): # inheritance
def __init__(self, title):
super().__init__(title, priority="Critical")
stc = SmokeTestCase("App loads successfully")
print(stc)
Why this matters beyond "QA scripting": Django is built entirely around classes (models, views) — this exact class/__init__/inheritance pattern is the syntax you'll be reading constantly in the Django phase. Don't skim this subsection.
requests library — hitting real APIs from Python Must Knowimport requests
response = requests.get("https://jsonplaceholder.typicode.com/users/1")
print(response.status_code) # 200
data = response.json()
print(data["name"])
# POST with a body
payload = {"title": "test post", "body": "hello", "userId": 1}
response = requests.post("https://jsonplaceholder.typicode.com/posts", json=payload)
print(response.status_code) # 201
This is the foundation of writing automated API tests in Python (an alternative/complement to Postman) — a genuinely valuable skill to show in your Portfolio Project 2 (Part 23).
is_valid_email(email) that does basic validation (contains exactly one @, has characters before and after it).["Pass", "Fail", "Pass", "Pass", "Fail"]) and returns a dict {"pass": 3, "fail": 2}.requests that GETs https://jsonplaceholder.typicode.com/users and prints the names of all users whose id is even.BugReport class with fields title, severity, priority, and a method summary() that returns a formatted one-line string.This module exists because your budget grew from ~125 to 200 hours — automation was cut from the original trimmed guide specifically to save time. With the extra room, hands-on automation is worth doing for real, not just conceptually. It stays 🟠 MUST KNOW, not 🔴 MUST MASTER: most BD entry-level manual/API QA roles still don't require it day one, but walking into an interview with a real, working automation suite on your GitHub is one of the sharpest differentiators available to a fresher.
Two automation tools done shallowly is weaker than one done properly — trying to hand-roll both Selenium and Playwright inside a tight budget was producing surface-level knowledge of each instead of a real, working suite of either. Playwright is the better single choice for this: it auto-waits for elements by default (removing the single biggest source of flaky, hard-to-debug tests), installs its own managed browsers so setup rarely breaks, and ships a built-in trace viewer and codegen recorder that make debugging dramatically easier for someone building their first suite. It's also the direction a growing share of newer BD teams and greenfield projects are adopting, so it reads as current, not dated.
Automation doesn't replace manual testing — it replaces the boring, repetitive parts of manual testing (regression) so a human tester has time for the parts that actually need judgment (exploratory, usability). Playwright is just the tool that drives a real browser — clicking, typing, reading the page — the way a script instead of a human.
Playwright launches and controls a real browser (Chromium, Firefox, or WebKit) over a fast, persistent connection, and drives it exactly the way a human would — clicking, typing, reading the rendered page. It does not talk to your app's backend directly; it only sees what the browser sees, which is precisely why it's valuable for QA: it tests the same path a real user takes.
from playwright.sync_api import sync_playwright, expect
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
page.goto("https://example.com/login")
page.get_by_label("Email").fill("qa.test@example.com")
page.get_by_label("Password").fill("Pass1234")
page.get_by_role("button", name="Login").click()
# Playwright auto-waits for this element before asserting -- no manual wait needed
expect(page.get_by_role("heading", name="Dashboard")).to_be_visible()
browser.close()
| Locator | Example | Stability |
|---|---|---|
get_by_role |
page.get_by_role("button", name="Login") |
Best — mirrors how a real user/screen-reader identifies the element, survives most styling/markup changes |
get_by_label |
page.get_by_label("Email") |
Excellent for form fields — matches the associated <label> |
get_by_test_id |
page.get_by_test_id("submit-btn") |
Best when available — a data-testid attribute added specifically for tests, immune to styling/copy changes |
get_by_text |
page.get_by_text("Forgot password?") |
Good for static, unique visible text |
| CSS selector | page.locator(".btn-primary") |
Fine as a fallback when nothing above fits; more brittle to styling changes |
A candidate who reaches for a raw CSS selector or XPath first, for everything, reads as inexperienced. The correct instinct with Playwright is get_by_role/get_by_label/get_by_test_id first — they're both more stable and more readable — with CSS only as a fallback when nothing semantic is available.
time.sleep(3): never do this. It's either too short (flaky failures) or wastes time (too long on every run, and at scale this compounds into hours of wasted CI time)..click(), .fill()) and every expect(...) assertion automatically retries until the element is actionable/visible or a timeout is hit. This single behavior removes the majority of "flaky test" bugs that plague junior-written Selenium suites — it's the main practical reason this module teaches only Playwright.# no manual wait needed anywhere here -- Playwright retries internally
page.get_by_role("button", name="Submit").click()
expect(page.get_by_text("Saved successfully")).to_be_visible()
Instead of scattering locators and interactions across every test, encapsulate one page's elements and actions inside a class. When the UI changes, you fix it in exactly one place instead of every test that touches that page.
# pages/login_page.py
class LoginPage:
def __init__(self, page):
self.page = page
self.email = page.get_by_label("Email")
self.password = page.get_by_label("Password")
self.submit = page.get_by_role("button", name="Login")
def login(self, email, password):
self.page.goto("https://example.com/login")
self.email.fill(email)
self.password.fill(password)
self.submit.click()
# tests/test_login.py
from pages.login_page import LoginPage
from playwright.sync_api import expect
def test_valid_login(page):
login = LoginPage(page)
login.login("qa.test@example.com", "Pass1234")
expect(page.get_by_role("heading", name="Dashboard")).to_be_visible()
def test_invalid_password(page):
login = LoginPage(page)
login.login("qa.test@example.com", "WrongPass")
expect(page.get_by_text("Incorrect email or password")).to_be_visible()
This is the exact shape a real BD test-automation team's repository looks like — small, readable, one page object per screen, one test file per feature.
qa-automation-suite/
pages/
login_page.py
dashboard_page.py
tests/
test_login.py
test_dashboard.py
conftest.py # pytest fixtures -- e.g. a shared `page` fixture
pytest.ini
requirements.txt
README.md
Run with pytest --html=report.html (via the pytest-html plugin) to produce a shareable HTML test report — exactly the kind of artifact worth linking from your portfolio project (see Part 23). Playwright's own trace viewer (playwright show-trace trace.zip) is also worth demonstrating — it replays exactly what the browser did, step by step, which is a genuinely strong thing to show live in an interview.
# .github/workflows/tests.yml
name: Run QA Suite
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install -r requirements.txt
- run: playwright install --with-deps
- run: pytest --html=report.html
A green checkmark on a GitHub repo, produced by a workflow you configured yourself, is worth far more in an interview than saying "I've read about test automation" — it's proof, not a claim.
Selenium is still what a share of existing BD QA job postings name explicitly (it's the incumbent, and legacy suites at established companies are written in it). You don't need hands-on Selenium experience for this guide's plan, but the underlying concepts transfer almost directly, so you won't be lost if it comes up:
| Playwright (what you learned) | Selenium equivalent (recognize, don't memorize) |
|---|---|
page.goto(url) | driver.get(url) |
page.get_by_role(...) / page.locator(...) | driver.find_element(By.ID, ...) / By.CSS_SELECTOR / By.XPATH |
| Automatic — built into every action/assertion | WebDriverWait(driver, 10).until(EC.<condition>) — you write waits by hand |
browser.close() | driver.quit() |
If a specific application you're applying to explicitly requires hands-on Selenium, treat that as a signal to deep-dive Selenium's syntax for that one application — the POM structure, waiting philosophy, and CI approach you already know from Playwright transfer over almost unchanged; only the exact API calls differ.
A junior candidate who says "I'm a Playwright expert" after one small suite gets caught out immediately by a follow-up question. The honest, stronger framing: "I understand manual testing is still the majority of the job, but I've built and can walk you through a small working Playwright automation suite — including POM structure, auto-waiting, and a CI pipeline that runs it on every push." That sentence, backed by a real repo link, outperforms a resume line that just says "Playwright" with nothing behind it.
.click() on an element.time.sleep() considered bad practice in automation, and how does Playwright avoid needing it?get_by_role, get_by_test_id, and a raw CSS selector by how stable they are, and explain why.Set up a real Playwright project against any public demo site (e.g. a demo login/e-commerce practice site). Write 3 tests: one happy-path login, one negative-path login (wrong password), and one that verifies a specific piece of text appears after an action. Get all 3 passing locally, then wire up the GitHub Actions workflow above so they run automatically on push. This suite becomes reusable evidence for Part 23's portfolio.
Part 2 already covers client-server architecture, HTTP, and JSON at the depth a working QA needs day-to-day. This module goes one layer deeper — into operating-system and networking fundamentals — specifically because stronger BD written exams (and interviewers at companies that also hire developers) sometimes probe this layer to gauge general CS literacy, not just QA-tool knowledge. Nothing here requires memorizing implementation details; it requires being able to explain the concept cleanly when asked.
You will not implement an operating system or a network stack. You will, regularly: read a stack trace that mentions a "segmentation fault" or "deadlock" and need to know roughly what that means; debug why a test environment behaves differently under load (often a resource/OS-level issue, not an application bug); explain to a developer that a bug reproduces "only over a slow network" (a networking-layer testing concern); and answer a written-exam section that separates candidates who only know QA tools from candidates who understand what's actually running underneath.
| Process | Thread | |
|---|---|---|
| Definition | An independent running program with its own memory space | A unit of execution within a process, sharing that process's memory |
| Isolation | Fully isolated from other processes (a crash in one usually doesn't crash another) | Threads in the same process share memory — a bug in one can corrupt shared state used by another |
| Communication | Needs explicit inter-process communication (sockets, pipes, shared files) | Can directly share variables/data (which is also why threading bugs are harder to find) |
| Cost | Heavier to create/switch between | Lighter — cheaper to create, faster to switch between |
a process is an isolated program; a thread is a lightweight worker inside that program, sharing its memory.
A memory leak in a long-running test automation suite (Part A) is a real, common failure mode — if your CI suite gets progressively slower over hundreds of runs, memory (not just test logic) is a reasonable first suspect.
The OS scheduler decides which process/thread gets CPU time next, using strategies like round-robin (each process gets a small time slice in turn) or priority-based scheduling. Awareness only at your level: know these terms exist and roughly what they mean if a written test asks a definitional question; you will not be asked to design a scheduler.
Real networking courses teach a 7-layer OSI model. For QA purposes, a simplified 4-layer mental model covers everything you'll actually be asked or need:
flowchart TD
A["Application layer
HTTP, HTTPS, DNS — what your browser/app speaks"]
B["Transport layer
TCP or UDP — how data gets broken up and delivered"]
C["Internet layer
IP — how data is addressed and routed between machines"]
D["Link layer
Wi-Fi, Ethernet — the physical/local network hop"]
A --> B --> C --> D
You already know the Application layer well from Part 2 (HTTP, status codes, JSON). This module fills in the layer just below it.
| TCP | UDP | |
|---|---|---|
| Guarantee | Reliable — guarantees delivery, in order, retransmits lost packets | No guarantee — packets can arrive out of order or be dropped entirely |
| Speed/overhead | Slower, more overhead (handshakes, acknowledgments) | Faster, minimal overhead |
| Used for | Web pages (HTTP/HTTPS), file transfer, anything where correctness matters more than speed | Video/voice calls, live streaming, online gaming — where a dropped packet is preferable to a delayed, stale one |
if you're ever asked to test a video call or live-streaming feature, understanding that it likely runs over UDP (not TCP) explains why occasional glitches/frozen frames are an accepted trade-off rather than always a bug — and why your test cases should focus on graceful degradation, not "zero packet loss ever."
Before any TCP data flows, the client and server exchange three messages to establish a connection: SYN (client: "let's connect") → SYN-ACK (server: "okay, let's connect") → ACK (client: "confirmed"). Awareness level: know it exists and why (it's how TCP establishes a reliable connection before trusting any data) — you won't be asked to diagram packet flags.
sequenceDiagram
participant Browser
participant DNS as DNS Resolver
participant Server
Browser->>DNS: "What's the IP for shop.example.com?"
DNS-->>Browser: "203.0.113.42"
Browser->>Server: HTTP request to 203.0.113.42
Server-->>Browser: HTTP response
DNS (Domain Name System) translates a human-readable domain into the IP address a computer actually connects to — already mentioned briefly in Part 2.9, expanded here because "explain what happens when you type a URL and press Enter" is one of the most common general-technical interview questions across both QA and dev roles, and DNS resolution is always expected as part of a complete answer.
localhost:8000 and localhost:5432 don't collide Must KnowA port is a number that identifies a specific service on a machine, letting one IP address run many independent network services simultaneously. Common ones worth recognizing cold: 80 (HTTP), 443 (HTTPS), 22 (SSH), 3306 (MySQL), 5432 (PostgreSQL), 5173/3000/8000 (common local dev-server defaults). When your test environment URL is staging.example.com:8080, the :8080 is telling your browser which service on that server to talk to.
This exact question (or a close variant) appears often enough across BD tech interviews — QA and dev alike — that it's worth having a clean, complete answer memorized:
Every single piece of this sentence maps to something you've already studied in this guide (Parts 2, 12, 15) or in B.6-B.9 above — this question is really just asking you to assemble the pieces into one coherent story.
HTTPS is HTTP sent over a TLS-encrypted connection. Practically, for testing purposes, this means: data in transit can't be read or tampered with by anyone intercepting the network traffic between client and server (a public Wi-Fi network, for instance). It does not mean the data is encrypted once it reaches the server's database — that's a separate concern (encryption at rest), and conflating the two is a common junior mistake worth avoiding out loud in an interview.
You already covered the practical command side of this in Part 13. The one conceptual piece worth adding: Linux file permissions (rwx for owner/group/others, as in -rwxr-xr-x) exist precisely to prevent one process or user from reading/modifying files it shouldn't — the same authorization principle from Part 2.8, applied at the operating-system level instead of the application level.
Open a terminal and run ping google.com, then nslookup google.com (or dig google.com if available). Write down, in your own words, what each command's output tells you and which layer of the B.6 model each one is operating at.
This entire chapter is intentionally conceptual, not hands-on. The goal: never be blindsided if these come up in an interview, without spending the 60+ hours real proficiency would take. If a specific job posting you're applying to explicitly requires hands-on Selenium or JMeter, treat that as a signal to deep-dive that one topic for that application — don't pre-invest broadly here.
Selenium WebDriver is a tool that programmatically controls a real browser — clicking, typing, reading page content — so test scripts can replay UI interactions automatically instead of a human doing it by hand.
Core concepts to recognize by name (not implement):
| Term | Meaning |
|---|---|
| WebDriver | The component that talks to and controls the browser |
| Locators | Ways to find an element on a page: id, name, class, CSS selector, XPath |
| XPath | A query language for navigating HTML/XML structure, e.g. //button[@id='submit'] |
| Explicit wait | Waiting for a specific condition (e.g., element becomes clickable) before proceeding — prevents flaky tests |
| Implicit wait | A global timeout applied to all element lookups |
| Page Object Model (POM) | A design pattern where each page of the app has a corresponding class encapsulating its elements/actions, keeping test code maintainable |
if the login button's HTML changes, without POM you'd have to fix that locator in every test file that clicks it; with POM, you fix it in one place (the LoginPage class). This design principle — separating "what a page can do" from "what a test asserts" — is worth understanding even without writing the code, because it's a genuinely good software engineering habit that also applies to Django's separation of concerns later.
"I've built a real, working automation suite with Playwright — including Page Object Model structure, auto-waiting, and a CI pipeline — and I understand Selenium's core concepts (locators, explicit/implicit waits, POM) even though my hands-on depth is specifically in Playwright. The underlying approach transfers directly if a role needs Selenium specifically."
| Term | Meaning |
|---|---|
| Performance testing | Umbrella term: how the system behaves under load (speed, stability) |
| Load testing | Behavior under expected/normal concurrent user load |
| Stress testing | Push beyond normal capacity to find the breaking point |
| Spike testing | Sudden, sharp increase in load, then back down |
| Endurance/soak testing | Sustained normal load over a long duration (finds memory leaks, slow degradation) |
| Response time | Time for a single request to get a response |
| Throughput | Requests handled per unit time |
| Concurrent users | How many users are active simultaneously |
| Bottleneck | The specific component (DB, network, CPU) limiting overall performance |
JMeter, at a glance: an open-source tool that simulates many virtual users hitting an application simultaneously, measuring response times/throughput/error rates under that load. You don't need to build a JMeter test plan for this guide's scope — just know what problem it solves and that it's the most common free tool for this in BD job postings that mention performance testing.
A junior QA is expected to recognize these risk categories during normal functional testing, not perform formal penetration testing.
| Concept | What a QA should recognize |
|---|---|
| SQL Injection | Malicious SQL smuggled through an input field (e.g., ' OR '1'='1) that could manipulate a database query if the backend doesn't properly sanitize/parameterize input |
| XSS (Cross-Site Scripting) | Malicious script (e.g., <script>alert(1)</script>) submitted through an input and later rendered/executed in another user's browser |
| CSRF (Cross-Site Request Forgery) | Tricking a logged-in user's browser into making an unwanted request to a site they're authenticated on |
| Password security | Passwords should never be visible in plaintext (in DB, logs, or network traffic over HTTP); should be hashed |
| Session management | Sessions should expire, logout should actually invalidate the session (not just hide the UI), tokens shouldn't be guessable |
| Access control (authZ) | A regular user should never be able to reach admin-only data/actions by directly manipulating a URL or API call |
| File upload security | Uploaded files should be validated server-side, not just by client-reported extension |
| Input validation | Every user input should be validated/sanitized server-side, never trusted from the client alone |
type ' OR '1'='1 or <script>alert(1)</script> into a normal text input during your own manual testing, and observe what happens. This costs nothing, requires no tooling, and is a real (if basic) security-awareness move that shows up favorably in interviews when you mention you do it as a habit.
flowchart LR
A[Code Commit] --> B[CI: Build]
B --> C[CI: Run Automated Tests]
C -->|Pass| D[CD: Deploy to Staging]
C -->|Fail| E[Notify Team, Block Merge]
D --> F[Manual/Exploratory QA on Staging]
F --> G[CD: Deploy to Production]
Deliberately trimmed — real DSA depth is deferred to your backend/Django interview prep later, where it will actually be weighted much more heavily. This section exists so a basic coding-screening round doesn't eliminate you.
Most broad BD manual/API QA roles do not test DSA at all. Some stronger companies (and Therap, per public reports) include a lighter programming/logic section — usually basic string/array manipulation and simple algorithmic thinking, not competitive-programming-level problems. The goal here is "don't freeze up," not "solve anything in under 15 minutes."
| Concept | What to know |
|---|---|
| Big-O basics | O(1), O(n), O(log n), O(n²) — recognize which is which for a piece of code, not derive formally |
| Arrays/Lists | Iteration, searching, basic manipulation |
| Strings | Reversal, palindrome check, character counting |
| Hash maps/sets (Python: dict/set) | Fast lookup, duplicate detection — this is the single highest-value DSA concept for QA-adjacent thinking, since it mirrors "find duplicates in test data" |
| Two pointers | Simple technique for sorted-array/string problems |
| Basic recursion | Factorial, Fibonacci — just enough to not be confused when you see it |
| Sorting | Know that sorted()/.sort() exist and roughly what "sorting takes O(n log n)" means — not implementing sort algorithms by hand |
| Binary search | The classic "guess the number" halving technique — very commonly asked as a simple standalone problem |
Explicitly not covered at this trim level (defer to backend-dev prep): trees, graphs/BFS/DFS, dynamic programming, advanced greedy algorithms, linked lists beyond basic awareness. If a specific job posting mentions these, treat that as a signal to study them for that interview specifically — don't pre-invest broadly.
| # | Problem | Difficulty | Concept | Why it matters for QA screening |
|---|---|---|---|---|
| 1 | Two Sum | Easy | Hash map | The single most commonly asked screening problem anywhere |
| 2 | Valid Palindrome | Easy | Two pointers, strings | Extremely common warm-up |
| 3 | Valid Anagram | Easy | Hash map/counting | Common, tests basic string/dict fluency |
| 4 | Contains Duplicate | Easy | Set/hash | Directly mirrors real QA "find duplicate data" thinking |
| 5 | Binary Search | Easy | Binary search | Classic; also a common conceptual interview question even without coding |
| 6 | Reverse String | Easy | Two pointers | Common basic warm-up |
| 7 | First Unique Character in a String | Easy | Hash map/counting | Common, practical string logic |
| 8 | Best Time to Buy and Sell Stock | Easy | Single-pass array logic | Frequently appears in "basic logic" screening rounds |
| 9 | Maximum Subarray | Easy/Medium | Array, basic DP intuition | Common, good stretch problem |
| 10 | Valid Parentheses | Easy | Stack | Very commonly asked, teaches stack concept cleanly |
| 11 | Merge Two Sorted Lists | Easy | Linked list basics | Common; enough linked-list exposure to not be blindsided |
| 12 | FizzBuzz | Easy | Loops/conditionals | Sounds trivial — is still occasionally used as a literal filter question; must be instant |
| 13 | Palindrome Number | Easy | Basic math/logic | Common variant of the palindrome theme |
| 14 | Roman to Integer | Easy | Hash map, string parsing | Common, tests careful logic under a slightly fiddly spec — good BVA-adjacent practice |
| 15 | Missing Number | Easy | Array/math | Common, has a satisfying "aha" trick worth learning |
| 16 | Two Sum II (sorted array) | Easy | Two pointers | Reinforces two-pointer technique in a different shape |
| 17 | Climbing Stairs | Easy | Basic recursion/DP intuition | The most commonly used "simple recursion" teaching problem |
| 18 | Number of 1 Bits | Easy | Bit basics | Occasionally appears; low effort to learn once |
don't grind all 18 back-to-back. Spread 2-3 per study session across weeks 8-9 of your schedule. For each: attempt for 15-20 minutes unaided first, then look up the pattern if stuck — the goal is pattern recognition, not raw memorization.
A realistic sprint, from a QA's chair:
| Audience | What they care about | How to adjust your communication |
|---|---|---|
| Developers | Exact technical reproduction steps, logs, root cause hints | Be precise, technical, non-judgmental — "I found X" not "you broke X" |
| Product Managers | Business impact, user experience risk | Lead with impact ("this blocks checkout for X% of users"), not technical detail |
| Business Analysts | Whether behavior matches requirements/acceptance criteria | Reference the specific acceptance criterion that's violated |
| QA Lead | Test coverage, risk areas, blockers | Be honest about what's NOT tested yet, not just what passed |
| Customers (rare, but possible) | Confidence the product works, plain language | No jargon, focus on outcomes not internals |
"How do you disagree professionally with a developer who says a bug isn't a bug?" Model answer approach: restate the acceptance criteria or expected behavior objectively, provide clear reproduction evidence, ask clarifying questions rather than asserting ("Can you help me understand the intended behavior here? Based on the acceptance criteria, I expected X but observed Y.") — avoid framing it as a personal disagreement; frame it as a shared question about what's correct.
"How do you report a critical bug found right before release?" Escalate immediately and directly (don't wait for the next standup) to whoever owns the release decision, with clear severity/impact framing, and let them make the release-vs-delay call — it's not solely your call, but it is your job to surface it loudly and immediately.
"What do you do when a developer says 'works on my machine'?" Provide exact environment details (browser/OS/build version), ask what their environment differs by, and offer to pair/screen-share to reproduce live — this phrase almost always means an environment or data difference, not that you're wrong.
"What do you do when requirements conflict or are ambiguous?" Don't guess and don't silently pick one interpretation — flag it explicitly to the PM/BA/team, ideally before you've built test cases around a wrong assumption, and document the clarification once you get it (in the ticket, not just verbally) so it doesn't get re-litigated later.
"How do you prioritize testing when time is limited?" Apply risk-based testing (Part 7.9) explicitly: highest business impact + highest likelihood of failure first; communicate clearly to the team what you did NOT get to test, rather than silently under-testing and letting people assume full coverage.
| Metric | Formula/meaning | Why it matters |
|---|---|---|
| Defect density | Defects ÷ size (e.g., per 1000 lines of code, or per module) | Identifies which modules are riskiest |
| Defect leakage | Defects found in production ÷ total defects found (including pre-release) | Measures how well testing caught issues before release |
| Test coverage | % of requirements/code covered by test cases | Identifies untested areas |
| Pass/fail rate | % of executed test cases that passed vs failed | Snapshot of current build health |
| Defect severity distribution | Breakdown of bugs by severity level | Shows overall build risk/quality at a glance |
| Reopen rate | % of "fixed" bugs that had to be reopened | Signals fix quality / dev-QA communication issues |
| Test execution progress | % of planned test cases executed so far vs. remaining | Tracks whether testing will finish on schedule |
These are asked about conceptually far more often than you'll be expected to calculate them from scratch at entry level — know what each measures and why a team would track it, more than memorizing exact formulas.
This is your final consolidation phase — everything above compressed into rapid-recall and exam-condition practice.
Don't just read the questions and nod. For each theory question, actually say your answer out loud or write it down before checking your understanding against the relevant Part above. For the mock exam, actually time yourself.
Fundamentals
Types of testing
Test design techniques
Process
Bug reporting
SQL/DB
API
General judgment
Use the framework from Part 9.2. These are deliberately varied — practice picking the framework apart differently each time rather than memorizing one script.
Beyond the 40 practice problems in Part 12.11, these are conceptual/verbal SQL questions common in interviews:
=?> and >> redirection?chmod +x script.sh do?.log files in a directory tree?ps aux show you?cat and less?|) do?df -h)curl to test a GET endpoint?== and is?self mean in a Python class method?requests?append() and extend() on a list?You don't need to hide your interest in backend development — but frame it honestly and professionally rather than either lying ("I want to be a QA lead forever") or bluntly stating "I'm using this as a stepping stone" (which reads as a flight risk). A strong, honest middle answer: "I want to build a strong foundation in quality and testing first, and I'm also naturally curious about the engineering side of things — long-term I could see myself growing into a role that combines both testing rigor and hands-on development, but right now my focus is becoming genuinely excellent at this role." This is true, doesn't overcommit, and doesn't raise a red flag.
This is a realistic simulation based on commonly-reported patterns in BD SQA written tests broadly (not a leaked or company-specific exam). Time yourself: 90 minutes total, no notes, as close to real exam conditions as you can manage.
Section A — QA Theory (10 questions, 15 minutes, short answers)
Section B — Test Case Writing (20 minutes) Write complete, professional test cases (using the structure from Part 8.1) for the following feature: "A 'Change Password' form requires the user to enter their current password, a new password (8-20 characters, at least 1 number), and confirm the new password. On success, show a confirmation message and log the user out of all other sessions." Write at least 6 test cases covering positive, negative, and boundary cases.
Section C — Bug Report Writing (10 minutes) You observe: when changing your password to one that's exactly 20 characters, the form silently fails with no error message and the password is not changed. Write a complete professional bug report for this.
Section D — SQL (20 minutes, 6 questions)
Given the users/orders schema from Part 12:
Section E — Linux (10 minutes, 4 questions)
.log files under /var/log.app.log.app.log.Section F — Logic/Coding (15 minutes, 2 questions)
Section A
Section B (sample strong test cases — yours don't need to match exactly, but should hit these categories)
Section C (sample strong bug report)
TitleChange Password form fails silently (no error, no success) when new password is exactly 20 characters
Environment: [fill with your actual test environment]
Preconditions: Logged in as a valid user
Steps to Reproduce: 1) Go to Change Password form. 2) Enter correct current password. 3) Enter a new password exactly 20 characters long, containing at least 1 number. 4) Enter the same value in Confirm Password. 5) Submit.
Expected Result: Password is changed successfully; confirmation message shown; other sessions logged out.
Actual Result: No confirmation message appears, no error message appears, and the password is not actually changed (verified by attempting login with the new password, which fails).
Severity: Major (a valid boundary-length password should be accepted; this silently breaks the feature with no user feedback)
Priority: High
Reproducibility: Always
Section D
1. SELECT * FROM users WHERE YEAR(created_at) = 2025;
2. SELECT user_id, SUM(amount) AS total FROM orders GROUP BY user_id HAVING SUM(amount) > 3000;
3. SELECT u.name FROM users u LEFT JOIN orders o ON u.id = o.user_id WHERE o.id IS NULL;
4. SELECT email, COUNT(*) FROM users GROUP BY email HAVING COUNT(*) > 1;
5. SELECT user_id, SUM(amount) AS total FROM orders GROUP BY user_id ORDER BY total DESC LIMIT 3;
6. WHERE filters individual rows before grouping; HAVING filters groups after aggregation — you can't use an aggregate function like SUM() inside WHERE.
Section E
1. find /var/log -name "*.log"
2. tail -n 100 app.log
3. grep -c "ERROR" app.log
4. chmod +x script.sh
Section F
1. def is_palindrome(s):
s = s.lower().replace(" ", "")
return s == s[::-1]
2. def first_unique_char(s):
from collections import Counter
counts = Counter(s)
for i, ch in enumerate(s):
if counts[ch] == 1:
return i
return -1
90%+ = excellent, apply with confidence. 70-89% = solid, review your weak sections above before applying. Below 70% = go back to the relevant Parts before moving to job applications — this exam mirrors real conditions closely enough that a low score here predicts a rough real written test.
Realistic simulation — practice out loud, ideally recorded, ideally with a friend reading questions to you.
Q1 is communication/structure. Q2-4 are theory fluency and applied thinking. Q5-6 are hard SQL skill checks — hesitation here is heavily penalized since SQL is considered "should be automatic" by many BD interviewers. Q7-8 test whether you can apply test design techniques outside a textbook example. Q9 tests whether you have genuine hands-on practice, not just reading. Q10 tests engagement/seriousness — always have 2-3 real questions ready (e.g., "what does the QA-developer collaboration process look like day to day?", "what testing tools does the team currently use?").
don't badmouth a previous team/professor/project; don't say "I don't have any weaknesses"; don't say salary "doesn't matter" (reads as either naive or dishonest); don't say you're only interested in automation/coding if applying for a manual QA role (reads as a flight risk, even if it's honestly your longer-term plan — the framing from 22.9 handles this better than blunt honesty would).
These live on your GitHub/CV and are what you point to when an interviewer asks "show me something you've actually built," not just "tell me what you know."
Pick a real, publicly usable website or app (e.g., a demo e-commerce site like Sauce Demo, or a real feature-limited free app) and produce a complete, professional manual QA documentation set for one meaningful feature area (e.g., login + cart + checkout).
manual-qa-project/
├── README.md
├── test-plan.md
├── test-cases.xlsx (or .csv/.md table)
├── bug-reports/
│ ├── BUG-001-cart-quantity-negative.md
│ └── ...
└── test-summary-report.md
what site was tested, why, how to read the test cases, a summary of key findings, and — importantly — what you'd test next if you had more time (shows judgment about scope, not just completeness).
walk through your thought process for 2-3 specific test cases (why you chose that boundary, why that negative case matters), and be ready to discuss one bug you found in detail, including its severity/priority reasoning.
A complete API test collection against a free public API (ReqRes or JSONPlaceholder), demonstrating both Postman skill and Python scripting skill — this dual demonstration is what makes this project stand out relative to typical junior portfolios.
.json) — from Part 16.4's mini-project, with positive/negative/boundary folders, environment variables, chained requests, and assertions.requests, from Part 17.10) that hits the same API and asserts the same core checks — showing you can do this outside Postman too.api-testing-project/
├── README.md
├── postman/
│ ├── collection.json
│ └── environment.json
├── python-tests/
│ ├── test_users_api.py
│ └── requirements.txt
└── results/
└── sample_run_output.txt
why you chose to test both via Postman and via Python (shows range), one specific negative/boundary case you're proud of, and how you'd extend this to a real authenticated API if given one.
The original scope included a 3rd Selenium automation project. Given your priority (fast QA job → Django transition), a shallow/rushed Selenium project would likely hurt more than help — a thin, obviously-templated automation project is a worse signal than no automation project plus two genuinely strong, deeply-understood ones. If you later decide to stay in QA longer than planned, adding a Selenium project is a natural next step, using the awareness foundation from Part 19.1.
Do this in your final week before applying, as a full-dress rehearsal.
"QuickCart" — a small e-commerce web app with: user registration/login, product browsing with category/price filters, a cart, checkout with a mock payment step, and an order history page. Assume a REST API backend and a PostgreSQL-style database with users, products, orders, and order_items tables.
POST /api/orders.Do this yourself, genuinely, before looking at any model answer approach. This capstone is intentionally close to the shape of a real take-home QA assessment some BD companies use — treating it as a dry run is exactly the point.
did you apply EP/BVA without being told to? Did your bug reports have precise steps and severity/priority reasoning? Did your SQL queries actually target data integrity, not just basic retrieval? Did your final report make an actual judgment call (not just list facts)? If yes to all four, you're genuinely at the "junior-ready" level this guide targets.
| Severity | Priority | |
|---|---|---|
| Answers | "How broken is it?" | "How fast do we fix it?" |
| Set by | QA | Business/Product (often) |
| Independent | Yes — can be High/Low in any combination |
New → Assigned → Open → Fixed → Retest → Closed (or Reopened → Assigned if retest fails); side paths: Rejected, Duplicate, Won't Fix.
| Smoke | Sanity | Regression | Retest |
|---|---|---|---|
| Broad, shallow, new build | Narrow, deeper, after small fix | Broad, after any change | Specific bug re-check after fix |
| Technique | Use when |
|---|---|
| EP | Reduce redundant tests across input ranges |
| BVA | Catch off-by-one bugs at edges |
| Decision Table | Multiple combined business rules |
| State Transition | Behavior depends on current state |
| Pairwise | Many independent variables combine |
| Risk-Based | Limited time, need to prioritize |
| 2xx Success | 3xx Redirect | 4xx Client Error | 5xx Server Error |
|---|---|---|---|
| 200 OK, 201 Created | 301, 304 | 400, 401, 403, 404, 409, 422, 429 | 500, 502, 503 |
SELECT col FROM table WHERE cond ORDER BY col LIMIT n;
SELECT col, COUNT(*) FROM table GROUP BY col HAVING COUNT(*) > n;
SELECT a.*, b.* FROM a INNER JOIN b ON a.id = b.a_id;
SELECT a.* FROM a LEFT JOIN b ON a.id = b.a_id WHERE b.id IS NULL; -- "no matching b" pattern
pwd; ls -la; cd path; mkdir dir; touch file
cp src dst; mv src dst; rm -r dir
cat file; less file; head -n 10 file; tail -f file
grep "text" file; find . -name "*.log"
chmod +x file; ps aux; kill -9 pid
curl -X GET url; command1 | command2; cmd > file; cmd >> file
git init / clone <url>
git status / add . / commit -m "msg"
git push / pull / fetch
git branch <name> / switch <name> / merge <name>
| Method | Idempotent | Use |
|---|---|---|
| GET | Yes | Retrieve |
| POST | No | Create |
| PUT | Yes | Full replace |
| PATCH | No | Partial update |
| DELETE | Yes | Remove |
d = {"k": "v"}; d.get("k", "default")
[x for x in items if condition] # list comprehension
try: ... except Exception as e: ... finally: ...
class Foo:
def __init__(self, x): self.x = x
Backlog → Sprint Planning → Sprint (Daily Standups) → Sprint Review → Retrospective, repeating. DoR = criteria before work starts; DoD = criteria before work is "complete."
| Resource | Topic | Essential/Optional | What to use it for |
|---|---|---|---|
| SQLZoo | SQL | Essential | Interactive SELECT/JOIN/aggregate practice |
| Postman Learning Center | API testing | Essential | Official docs, walk through collections/environments/tests |
| ReqRes / JSONPlaceholder | API testing practice | Essential | Free public APIs for your Portfolio Project 2 |
| LeetCode | DSA | Essential (light use) | The 18-problem list in Part 18.3 |
| GitHub Skills | Git | Essential if new to Git | Free interactive lessons |
| MDN Web Docs | HTTP/web fundamentals | Optional | Reference for HTTP status codes, headers |
| Python official docs | Python | Optional/reference | Look up exact syntax when needed, not a start-to-finish read |
| PostgreSQL docs | SQL/DB | Optional/reference | Same DB you'll likely use with Django later |
| NeetCode.io | DSA | Optional | Video explanations if a LeetCode pattern doesn't click |
| Playwright official docs (Python) | Automation (Part A) | Essential for Part A | Clear, example-heavy — the "Writing tests" and "Locators" pages map directly onto A.3-A.5 |
| Selenium official docs | Automation awareness | Optional, later | Only if a specific job posting requires hands-on Selenium — see A.8 |
| Django official docs/tutorial | Backend transition | Essential — for Phase 2, not now | Start here in Part 26's Django track |
| Score | Level |
|---|---|
| 0-49% | Beginner — keep working through the guide systematically |
| 50-69% | Developing — identify your weakest 2-3 checklist items and focus there |
| 70-84% | Junior-ready — start applying, keep sharpening weak spots in parallel |
| 85-94% | Strong candidate — apply broadly and confidently |
| 95-100% | Highly prepared — you're ready for stronger companies too |
Don't wait for 95-100% to start applying. Job searching and interview cycles take weeks in BD; at 70%+ you should already be applying while continuing to sharpen the remaining items — the interview process itself is also valuable practice.
This is the part that makes this whole guide make sense as a strategy, not just a QA course. Read this now, but really revisit it once you have the job.
You'll be working full-time as a QA. The realistic hour budget for learning Django alongside a full-time job is roughly 1-2 hours/day on weekdays, more on weekends — call it 10-14 hours/week, similar or slightly less than your current pace since you'll also be adjusting to a new job. At that pace, a focused Django curriculum (roughly 150-180 hours, since backend dev requires more hands-on building than QA theory) takes ~12-16 weeks, comfortably inside your 4-5 month target.
Because of how this guide was built, you walk into the Django phase already knowing: Python fundamentals through OOP, SQL (deeply — directly maps to Django's ORM concepts), Git workflow, HTTP/REST/API concepts (you'll now be building the APIs you used to test), and JSON. This is a genuinely significant head start most Django beginners don't have — lean into it rather than re-studying these from a "backend dev" book from scratch.
This is the last part of the guide. Good luck — start September 25.
This deck pulls exactly the facts Part 1.3 of the guide itself flags as "memorize — know cold, recall instantly": severity/priority, the defect lifecycle, SDLC/STLC order, HTTP status codes, core terminology, and the highest-yield facts from the new Automation and CS Fundamentals modules. Nothing here is new content — it's the guide's own facts, in a format built for spaced repetition instead of re-reading.