Quality Assurance engineering is one of the most stable and consistently in-demand roles in tech. Every software team needs QA, yet it's far less crowded than software engineering. This roadmap shows you exactly what to learn, in what order — from zero manual tester to automation engineer with a full CI/CD pipeline, in 9–12 months.
At a glance
| Phase | Topics | Time estimate |
|---|---|---|
| 1 | Testing fundamentals and SDLC | 3–4 weeks |
| 2 | Manual testing skills | 4–5 weeks |
| 3 | SQL and databases for testers | 2–3 weeks |
| 4 | API testing (Postman, REST) | 3–4 weeks |
| 5 | Programming basics (Python or JavaScript) | 6–8 weeks |
| 6 | Selenium WebDriver and automation frameworks | 6–8 weeks |
| 7 | Modern automation: Playwright, Cypress | 4–6 weeks |
| 8 | Performance and load testing | 3–4 weeks |
| 9 | CI/CD integration and DevOps basics | 3–4 weeks |
| 10 | Mobile testing | 2–3 weeks |
| 11 | Certifications and portfolio | 3–4 weeks |
| Total to first automation role | ~9–12 months |
Phase 1 — Testing fundamentals and SDLC
Before you write a single test, you need to understand what testing is for and where it fits in the software development lifecycle.
SDLC models
| Model | Description | When used |
|---|---|---|
| Waterfall | Sequential phases, testing at end | Legacy, regulated industries |
| Agile (Scrum/Kanban) | Iterative sprints, continuous testing | Most modern teams |
| V-Model | Testing mapped to each dev phase | Safety-critical systems |
| DevOps / CI-CD | Automated tests run every commit | High-release-velocity teams |
Test types hierarchy
E2E / UI tests ← few, slow, expensive (UI layer)
↑
Integration tests ← moderate, test component interaction
↑
Unit tests ← many, fast, cheap (function level)
The Testing Pyramid principle: most tests should be unit tests. Heavy reliance on E2E tests is the most common anti-pattern.
Core vocabulary every QA must know
| Term | Definition |
|---|---|
| Test case | Step-by-step instructions to verify a specific behaviour |
| Test suite | Collection of related test cases |
| Bug / defect | Deviation from expected behaviour |
| Regression | Bug introduced by a change that worked before |
| Smoke test | Quick check that core functionality works after a build |
| Sanity test | Narrow check after a bug fix or minor change |
| Exploratory testing | Unscripted, creative testing without predefined cases |
| UAT | User Acceptance Testing — final check before release |
| Severity vs priority | Severity = impact on user; Priority = urgency of fix |
Bug reporting best practice
A good bug report contains:
- Title — short, specific ("Login button unresponsive on Safari 17")
- Environment — OS, browser, app version, device
- Preconditions — account type, test data needed
- Steps to reproduce — numbered, exact
- Expected result — what should happen
- Actual result — what actually happens
- Severity/Priority — your assessment
- Attachments — screenshots, video, logs
Phase 2 — Manual testing skills
Automation doesn't replace manual testing — it frees your time for the testing only humans can do.
Test design techniques
| Technique | What it tests | Example |
|---|---|---|
| Equivalence partitioning | Valid + invalid input classes | Age field: <0, 0–120, >120 |
| Boundary value analysis | Edges of valid range | 0, 1, 119, 120, 121 |
| Decision table | Combinations of conditions/actions | Login with 2FA: 4 combinations |
| State transition | Object moves between states | Order: Created→Paid→Shipped→Delivered |
| Pairwise / combinatorial | All pairs of input combinations | Reduces 1000s of cases to ~30 |
Test case writing template
ID: TC-LOGIN-001
Title: Valid credentials login
Preconditions: User account exists, email=test@example.com, pass=Test1234!
Steps:
1. Navigate to /login
2. Enter email: test@example.com
3. Enter password: Test1234!
4. Click "Log in"
Expected: User redirected to /dashboard, username visible in header
Priority: High
What to test beyond the happy path
- Negative tests — invalid inputs, wrong passwords, missing required fields
- Edge cases — empty strings, null, very long inputs, special characters (emoji, SQL)
- Boundary conditions — min/max values, 0, -1, empty arrays
- Concurrency — two users editing the same record simultaneously
- Security basics — XSS in input fields, path traversal, IDOR (object reference)
- Accessibility — keyboard navigation, screen reader labels, colour contrast
Phase 3 — SQL and databases for testers
QA engineers constantly need to verify data in the database — don't rely on the UI alone.
Essential SQL for QA
-- Verify user was created
SELECT id, email, created_at FROM users WHERE email = 'test@example.com';
-- Check order total matches UI
SELECT SUM(price * quantity) as total
FROM order_items WHERE order_id = 12345;
-- Find orphaned records (data integrity check)
SELECT o.id FROM orders o
LEFT JOIN users u ON o.user_id = u.id
WHERE u.id IS NULL;
-- Count records for pagination validation
SELECT COUNT(*) FROM products WHERE category = 'electronics';
-- Verify soft-delete logic
SELECT id, deleted_at FROM posts WHERE id = 99;
Joins testers must know
| Join type | Returns |
|---|---|
| INNER JOIN | Only matching rows in both tables |
| LEFT JOIN | All rows from left + matches from right |
| RIGHT JOIN | All rows from right + matches from left |
| FULL OUTER JOIN | All rows from both, nulls for mismatches |
Test data management
- Use dedicated test databases — never test against production
- Reset test data between test runs for repeatability
- Use factories/fixtures to generate consistent test data
- Be careful with PII — anonymize production dumps
Tools: Faker (Python/JS) generates realistic fake data; DB Fiddle for quick SQL practice.
Phase 4 — API testing
Most modern apps are built on APIs. API testing is faster, more reliable, and finds bugs that UI tests miss.
REST API basics for testers
| Method | Purpose | Success code |
|---|---|---|
| GET | Read resource | 200 |
| POST | Create resource | 201 |
| PUT | Replace resource | 200 |
| PATCH | Partial update | 200 |
| DELETE | Remove resource | 204 |
HTTP status codes you must know
| Code | Meaning | What to test |
|---|---|---|
| 200 | OK | Body matches spec |
| 201 | Created | Location header, body has new ID |
| 400 | Bad request | Error message is descriptive |
| 401 | Unauthorized | No/invalid token |
| 403 | Forbidden | Valid token, wrong permissions |
| 404 | Not found | ID doesn't exist |
| 409 | Conflict | Duplicate resource |
| 422 | Unprocessable | Validation errors in body |
| 500 | Server error | Should never leak stack traces |
Postman workflow
Collection: User API
├── Folder: Auth
│ ├── POST /auth/register → save {{token}} to env
│ └── POST /auth/login → save {{token}} to env
├── Folder: Users
│ ├── GET /users/me → verify fields
│ ├── PUT /users/me → update + verify
│ └── DELETE /users/me → 204 + verify gone
└── Folder: Edge cases
├── POST /auth/login (wrong password) → 401
├── GET /users/99999 (missing) → 404
└── POST /users (duplicate email) → 409
Pre-request scripts — set up auth tokens, timestamps, unique emails:
pm.environment.set("unique_email", `test_${Date.now()}@example.com`);
Test scripts — assert responses in Postman:
pm.test("Status is 200", () => pm.response.to.have.status(200));
pm.test("Has user id", () => {
const body = pm.response.json();
pm.expect(body.id).to.be.a("number");
pm.expect(body.email).to.equal(pm.environment.get("test_email"));
});
pm.environment.set("user_id", pm.response.json().id);
Newman — run Postman in CI
# Install
npm install -g newman
# Run collection
newman run my-collection.json -e staging.json --reporters cli,junit --reporter-junit-export results.xml
Phase 5 — Programming basics (Python or JavaScript)
You need one language well enough to write automation. Python is the most popular QA choice; JavaScript if you're testing a JS-heavy app.
Python for QA — the essentials
# String manipulation
url = f"https://api.example.com/users/{user_id}"
assert "error" not in response.text.lower()
# List comprehension
failed_tests = [t for t in results if t["status"] == "failed"]
# File handling — read test data CSV
import csv
with open("test_data.csv") as f:
users = list(csv.DictReader(f))
# HTTP requests — manual API testing
import requests
response = requests.post(
"https://api.example.com/auth/login",
json={"email": "test@example.com", "password": "Test1234!"}
)
assert response.status_code == 200
token = response.json()["token"]
# Environment variables — keep secrets out of code
import os
base_url = os.getenv("API_BASE_URL", "https://staging.example.com")
JavaScript for QA (Node.js)
// Fetch API
const response = await fetch(`${BASE_URL}/users/${userId}`, {
headers: { Authorization: `Bearer ${token}` }
});
const user = await response.json();
console.assert(user.email === expectedEmail);
// Array methods
const failedCases = results.filter(r => r.status !== "passed");
const emails = users.map(u => u.email);
// Async/await pattern common in Playwright/Cypress
async function login(page, email, password) {
await page.fill('[name="email"]', email);
await page.fill('[name="password"]', password);
await page.click('[type="submit"]');
await page.waitForURL("**/dashboard");
}
Phase 6 — Selenium WebDriver and automation frameworks
Selenium is the foundation of browser automation. Even if you move to Playwright later, understanding Selenium makes you stronger.
Selenium with Python (pytest)
# conftest.py — shared fixtures
import pytest
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
@pytest.fixture(scope="session")
def driver():
options = Options()
options.add_argument("--headless")
options.add_argument("--no-sandbox")
driver = webdriver.Chrome(options=options)
driver.implicitly_wait(10)
yield driver
driver.quit()
# test_login.py
from selenium.webdriver.common.by import By
def test_valid_login(driver):
driver.get("https://example.com/login")
driver.find_element(By.NAME, "email").send_keys("test@example.com")
driver.find_element(By.NAME, "password").send_keys("Test1234!")
driver.find_element(By.CSS_SELECTOR, "[type='submit']").click()
assert "/dashboard" in driver.current_url
def test_invalid_login_shows_error(driver):
driver.get("https://example.com/login")
driver.find_element(By.NAME, "email").send_keys("wrong@example.com")
driver.find_element(By.NAME, "password").send_keys("wrongpass")
driver.find_element(By.CSS_SELECTOR, "[type='submit']").click()
error = driver.find_element(By.CLASS_NAME, "error-message")
assert "Invalid credentials" in error.text
Locator strategy priority
| Priority | Locator | Example | Why |
|---|---|---|---|
| 1st | data-testid / data-cy |
[data-testid="submit-btn"] |
Stable, purpose-built |
| 2nd | id |
#submit-button |
Unique, fast |
| 3rd | name |
[name="email"] |
Semantic HTML |
| 4th | CSS selector | .btn-primary |
Readable, flexible |
| 5th | XPath | //button[text()='Submit'] |
Last resort |
| Avoid | XPath with index | //div[3]/span[1] |
Brittle, breaks on layout change |
Page Object Model (POM)
The most important pattern in test automation — keeps tests readable and maintainable.
# pages/login_page.py
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
class LoginPage:
URL = "/login"
def __init__(self, driver):
self.driver = driver
self.wait = WebDriverWait(driver, 10)
def open(self, base_url):
self.driver.get(f"{base_url}{self.URL}")
return self
def enter_email(self, email):
self.driver.find_element(By.NAME, "email").send_keys(email)
return self
def enter_password(self, password):
self.driver.find_element(By.NAME, "password").send_keys(password)
return self
def submit(self):
self.driver.find_element(By.CSS_SELECTOR, "[type='submit']").click()
return self
def get_error(self):
return self.wait.until(
EC.visibility_of_element_located((By.CLASS_NAME, "error-message"))
).text
# tests/test_login.py
def test_valid_login(driver, base_url):
page = LoginPage(driver).open(base_url)
page.enter_email("test@example.com").enter_password("Test1234!").submit()
assert "/dashboard" in driver.current_url
Phase 7 — Modern automation: Playwright and Cypress
Playwright and Cypress have largely replaced Selenium for new projects. They're faster, more reliable, and have better developer experience.
Playwright (Python or TypeScript)
// tests/login.spec.ts
import { test, expect } from "@playwright/test";
test.describe("Login", () => {
test("valid credentials → redirect to dashboard", async ({ page }) => {
await page.goto("/login");
await page.fill('[name="email"]', "test@example.com");
await page.fill('[name="password"]', "Test1234!");
await page.click('[type="submit"]');
await expect(page).toHaveURL(/dashboard/);
});
test("invalid password → error message", async ({ page }) => {
await page.goto("/login");
await page.fill('[name="email"]', "test@example.com");
await page.fill('[name="password"]', "wrongpassword");
await page.click('[type="submit"]');
await expect(page.locator(".error-message")).toContainText("Invalid");
});
test("API-seeded user login", async ({ page, request }) => {
// Create user via API — faster than UI setup
const res = await request.post("/api/users", {
data: { email: `test_${Date.now()}@example.com`, password: "Test1234!" }
});
const { email } = await res.json();
await page.goto("/login");
await page.fill('[name="email"]', email);
await page.fill('[name="password"]', "Test1234!");
await page.click('[type="submit"]');
await expect(page).toHaveURL(/dashboard/);
});
});
playwright.config.ts
import { defineConfig, devices } from "@playwright/test";
export default defineConfig({
testDir: "./tests",
timeout: 30_000,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 4 : undefined,
reporter: [["html"], ["junit", { outputFile: "results.xml" }]],
use: {
baseURL: process.env.BASE_URL ?? "http://localhost:3000",
trace: "on-first-retry",
screenshot: "only-on-failure",
video: "retain-on-failure",
},
projects: [
{ name: "chromium", use: { ...devices["Desktop Chrome"] } },
{ name: "firefox", use: { ...devices["Desktop Firefox"] } },
{ name: "webkit", use: { ...devices["Desktop Safari"] } },
{ name: "mobile-chrome", use: { ...devices["Pixel 5"] } },
],
});
Playwright vs Cypress at a glance
| Feature | Playwright | Cypress |
|---|---|---|
| Multi-browser | Chrome, Firefox, Safari | Chrome, Edge, Firefox (no Safari) |
| Multi-tab | Yes | Limited |
| Language support | JS/TS, Python, Java, C# | JS/TS only |
| Component testing | Yes (experimental) | Yes (mature) |
| Network interception | Yes | Yes |
| Parallel execution | Built-in, fast | Cypress Cloud (paid) |
| Learning curve | Moderate | Easier |
| Best for | Cross-browser, multi-lang teams | JS teams, great DX |
Phase 8 — Performance and load testing
Find bottlenecks before users do.
k6 — load testing as code
// load-test.js
import http from "k6/http";
import { check, sleep } from "k6";
import { Rate } from "k6/metrics";
const errorRate = new Rate("errors");
export const options = {
stages: [
{ duration: "30s", target: 20 }, // ramp up to 20 users
{ duration: "1m", target: 20 }, // stay at 20 users
{ duration: "30s", target: 100 }, // spike to 100 users
{ duration: "30s", target: 0 }, // ramp down
],
thresholds: {
http_req_duration: ["p(95)<500"], // 95% of requests < 500ms
errors: ["rate<0.01"], // error rate < 1%
},
};
export default function () {
const res = http.get("https://staging.example.com/api/products");
const ok = check(res, {
"status is 200": r => r.status === 200,
"response time < 500ms": r => r.timings.duration < 500,
});
errorRate.add(!ok);
sleep(1);
}
k6 run load-test.js
k6 run --out json=results.json load-test.js
Key performance metrics
| Metric | Description | Good target |
|---|---|---|
| Response time p50 | Median response time | <200ms |
| Response time p95 | 95th percentile | <500ms |
| Response time p99 | 99th percentile | <1000ms |
| Throughput (RPS) | Requests per second | Depends on SLA |
| Error rate | % requests that failed | <0.1% |
| TTFB | Time to first byte | <100ms |
| Apdex | User satisfaction score 0–1 | >0.9 |
Types of performance tests
| Test type | Goal |
|---|---|
| Load test | Verify normal expected load |
| Stress test | Find breaking point beyond expected load |
| Spike test | Sudden traffic burst (flash sale, news event) |
| Soak test | Sustained load over hours — finds memory leaks |
| Volume test | Large datasets — pagination, DB query performance |
Phase 9 — CI/CD integration
Automated tests only deliver value when they run automatically on every change.
GitHub Actions — full QA pipeline
# .github/workflows/qa.yml
name: QA Pipeline
on:
push:
branches: [main, develop]
pull_request:
jobs:
unit-tests:
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: pytest tests/unit/ --junitxml=unit-results.xml --cov=src --cov-report=xml
- uses: actions/upload-artifact@v4
with:
name: unit-test-results
path: unit-results.xml
api-tests:
runs-on: ubuntu-latest
needs: unit-tests
steps:
- uses: actions/checkout@v4
- run: pip install requests pytest
- run: pytest tests/api/ --junitxml=api-results.xml
env:
API_BASE_URL: ${{ secrets.STAGING_URL }}
API_TOKEN: ${{ secrets.STAGING_TOKEN }}
e2e-tests:
runs-on: ubuntu-latest
needs: api-tests
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- run: npm ci
- run: npx playwright install --with-deps chromium
- run: npx playwright test --reporter=html
env:
BASE_URL: ${{ secrets.STAGING_URL }}
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-report/
performance-test:
runs-on: ubuntu-latest
needs: e2e-tests
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- run: |
curl https://github.com/grafana/k6/releases/download/v0.52.0/k6-v0.52.0-linux-amd64.tar.gz -L | tar xvz
sudo mv k6-*/k6 /usr/local/bin/
- run: k6 run tests/performance/load-test.js
Test pyramid in CI — timing strategy
| Stage | Tests | When | Target time |
|---|---|---|---|
| Pre-commit (local) | Unit tests | Every save | <10s |
| PR check | Unit + integration | Every PR | <5 min |
| Merge to develop | Full E2E (smoke) | Every merge | <15 min |
| Nightly | Full E2E + regression | Every night | <1 hour |
| Release gate | E2E + performance | Before release | <2 hours |
Phase 10 — Mobile testing
Mobile apps have unique failure modes: different screen sizes, OS versions, touch gestures, interruptions.
Appium basics
# Mobile web on Android Chrome
from appium import webdriver
from appium.options import UiAutomator2Options
options = UiAutomator2Options()
options.platform_name = "Android"
options.device_name = "emulator-5554"
options.browser_name = "Chrome"
driver = webdriver.Remote("http://localhost:4723", options=options)
driver.get("https://m.example.com/login")
driver.find_element("id", "com.example:id/email").send_keys("test@example.com")
driver.find_element("id", "com.example:id/password").send_keys("Test1234!")
driver.find_element("id", "com.example:id/login_btn").click()
Device farms
| Service | Best for |
|---|---|
| BrowserStack | Real devices, cross-browser, Playwright/Appium |
| Sauce Labs | Enterprise, extensive device coverage |
| AWS Device Farm | AWS-native, pay per minute |
| Firebase Test Lab | Android/Flutter, Google ecosystem |
| Emulators/simulators | Fast local dev (not production equivalent) |
Mobile-specific test areas
- Screen sizes — phone, tablet, foldable
- OS versions — always test last 2–3 major versions
- Orientation — portrait and landscape
- Network conditions — 4G, 3G, offline, network switch
- Interruptions — incoming call, low battery warning, notifications
- Permissions — location, camera, contacts denied/granted
- Deep links — open app from URL or push notification
Phase 11 — Certifications
Certifications aren't mandatory, but they demonstrate structured knowledge — especially valuable early in your career.
| Certification | Issuer | Level | Cost | Focus |
|---|---|---|---|---|
| ISTQB Foundation (CTFL) | ISTQB | Beginner | ~$200 | Testing fundamentals, universal |
| ISTQB Advanced (CTAL) | ISTQB | Intermediate | ~$400 | Test manager, test analyst, tech test analyst |
| AWS Certified Cloud Practitioner | AWS | Beginner | $100 | Cloud basics — useful for cloud QA |
| Certified Agile Tester (CAT) | iSQI | Intermediate | ~$300 | Testing in Agile/Scrum |
| Selenium WebDriver with Python | Udemy cert | — | ~$15–20 | Not an industry cert, but portfolio-relevant |
| ISTQB CTFL-AT (Agile Extension) | ISTQB | Intermediate | ~$200 | Agile testing specialization |
ISTQB Foundation (CTFL) is the most recognized globally. Start here.
QA career roles and salaries
| Role | Description | Typical salary (US) |
|---|---|---|
| Junior QA / Manual Tester | Test case writing, bug reporting, manual E2E | $45k–65k |
| QA Engineer | Mix of manual + some automation | $65k–90k |
| QA Automation Engineer | Framework development, CI integration | $85k–115k |
| SDET (Software Dev Engineer in Test) | Deep coding skills, builds test infrastructure | $100k–140k |
| Performance Engineer | Load testing, profiling, capacity planning | $90k–130k |
| QA Lead / Manager | Team leadership, process design, stakeholder communication | $100k–150k |
Full technology map
QA Engineering
├── Manual testing
│ ├── Test design (EP, BVA, decision tables, state transition)
│ ├── Bug reporting (JIRA, Linear, GitHub Issues)
│ └── Exploratory testing
│
├── API testing
│ ├── Postman / Bruno / Insomnia
│ ├── REST + GraphQL + gRPC
│ └── Newman (CI runner)
│
├── UI Automation
│ ├── Selenium WebDriver
│ ├── Playwright (cross-browser, multi-lang)
│ └── Cypress (JS-native, great DX)
│
├── Mobile testing
│ ├── Appium
│ ├── Detox (React Native)
│ └── XCUITest / Espresso (native)
│
├── Performance testing
│ ├── k6
│ ├── JMeter
│ └── Locust (Python)
│
├── CI/CD
│ ├── GitHub Actions
│ ├── Jenkins
│ └── GitLab CI
│
└── Supporting skills
├── SQL (data verification)
├── Linux / command line
├── Docker (test environments)
└── Git (version control)
Realistic 12-month timeline
| Month | Focus | Milestone |
|---|---|---|
| 1 | Testing fundamentals, SDLC, ISTQB prep | First bug report written |
| 2 | Manual testing, test case design | 20 test cases for a real project |
| 3 | SQL basics, API concepts | Can query a DB and verify data manually |
| 4 | Postman, REST API testing | Full Postman collection with test scripts |
| 5–6 | Python/JS basics | Can write loops, functions, handle HTTP |
| 7–8 | Selenium + pytest / POM | 10 automated UI tests running locally |
| 9 | Playwright or Cypress | Same tests migrated, CI pipeline running |
| 10 | k6 performance testing | Load test for one critical endpoint |
| 11 | Portfolio project + ISTQB study | Public GitHub repo with full test suite |
| 12 | Job applications | First QA/automation role |
Portfolio project idea
Build a full test suite for an open-source app (like Conduit or OpenCart demo):
qa-portfolio/
├── README.md # What you tested, tech stack, how to run
├── test-plan.md # Scope, approach, entry/exit criteria
├── manual/
│ └── test-cases.xlsx # Or test-cases.md — 30+ manual test cases
├── api/
│ ├── postman/
│ │ └── collection.json # Exported Postman collection
│ └── tests/
│ └── test_api.py # pytest + requests
├── ui/
│ ├── pages/ # Page Object Model classes
│ └── tests/ # Playwright or Selenium tests
├── performance/
│ └── load-test.js # k6 script
└── .github/
└── workflows/
└── qa.yml # CI pipeline that runs all tests
Common mistakes QA beginners make
| Mistake | Why it hurts | Fix |
|---|---|---|
| Testing only the happy path | Bugs hide in edge cases | Always write at least 3 negative tests per feature |
| Automating everything | Slow suite, high maintenance | Automate what's stable and high-value; explore manually |
| Ignoring test data management | Flaky tests, random failures | Use factories, seed data, reset between runs |
| Skipping API tests | Missing a whole bug category | Test API independently of UI |
| Brittle locators (XPath indexes) | Breaks on any layout change | Use data-testid attributes |
| No assertions — only navigation | Test passes even when app is wrong | Every test must assert something |
| Not reading error messages | Slower debugging | Always read the full error before Googling |
| Testing in production | Risk of real user impact | Always use staging/test environments |
Frequently asked questions
Do I need to code to become a QA engineer? Not for manual testing, but automation engineering (where salaries are 30–50% higher) requires programming. Python or JavaScript is enough.
Is manual testing dead? No. Exploratory testing, usability assessment, accessibility review, and edge-case discovery still require human judgment. Automation handles repetitive regression; humans handle creative testing.
Selenium vs Playwright — which should I learn first? Playwright if you're starting from scratch in 2025. It's the modern standard. Learning Selenium is still valuable if your job uses it, but Playwright wins in new projects.
How long to get my first QA job? 6–9 months for manual QA. 9–12 months for automation roles. Start applying around month 8 — the job search itself takes time.
ISTQB — is it worth it? Yes, especially for the Foundation level. It provides a universal vocabulary, is recognized globally, and signals commitment. Cost is ~$200, and you can pass with 2–3 weeks of study.
QA vs SDET — what's the difference? QA Engineer focuses on testing strategy, test cases, and automation of test scenarios. SDET (Software Dev Engineer in Test) writes test infrastructure, frameworks, and tools — closer to a software engineer role. SDETs usually earn 15–25% more.