00 · Foundations & Mindset
6 topics
Why Test Automation Matters
Concept
Manual testing scales linearly with the product; automation scales with engineering effort. Without automation, every release re-tests the same flows by hand, regression coverage shrinks under deadline pressure, and bugs reach production. The four returns automation gives you:
Example — the cost curve
- Speed — what takes a tester 2 hours runs in 2 minutes, on every commit.
- Repeatability — the same inputs, the same assertions, every time. Humans drift; suites don't.
- Confidence to change — refactors and migrations stop being scary because the safety net is real.
- Living documentation — a well-named test suite tells a new engineer what the system actually does.
Manual regression of 50 flows Per release: 16 hours / human 10 releases: 160 hours Bug-escape rate: ~12% (fatigue + skipped flows) Automated regression of 50 flows Build time: ~40 hours one-time Per release: ~10 minutes (CI) Bug-escape rate: ~3% (when paired with manual exploratory)Exercise
- Pick one manual flow your team runs every release. Time it. Multiply by # of releases per year.
- Estimate the build cost of automating it. Compute payback period in releases.
- Identify one flow that should NOT be automated yet (too volatile, low value, hard to stabilize) — explain why.
The Practical Test Pyramid
martinfowler.com
Why automate, what to automate, at which level. Foundational reading.
Google Engineering Practices
google.github.io/eng-practices
How a high-throughput org thinks about test ROI and review.
Test Automation Canvas
ministryoftesting.com
A worksheet for justifying what to automate, and what not to.
The Automation Pyramid
Concept
The automation pyramid (Mike Cohn, refined by Martin Fowler) is the canonical model for distributing your test types. From bottom to top:
The anti-pattern is the inverted pyramid (or "ice cream cone"): few unit tests, many slow E2Es. Symptoms: 30-minute CI, intermittent reds nobody trusts, regressions that still ship.
Example — what the pyramid looks like
- Unit tests — many, fast (ms), test one function/class in isolation. The base.
- Integration / Component tests — fewer, test how units interact (a service + its DB, a component + its API client).
- API / Contract tests — verify the boundaries between services without launching a UI.
- End-to-End (E2E) UI tests — few, slow, fragile, but only these prove the whole system works for a real user.
The anti-pattern is the inverted pyramid (or "ice cream cone"): few unit tests, many slow E2Es. Symptoms: 30-minute CI, intermittent reds nobody trusts, regressions that still ship.
Speed: fast ←─────────────────────── slow Cost: cheap ←────────────────────── expensive Coverage: narrow ←───────────────────── broad
- Count tests in your current project at each level. Sketch the actual shape.
- Find one E2E test that asserts a business rule. Could it move to unit/integration? Try it.
- Pick one bug from the last sprint. At which level should it have been caught?
The Practical Test Pyramid — Fowler
martinfowler.com
The most-cited modern framing. Required reading.
Just Say No to More End-to-End Tests
Google Testing Blog
Counter-intuitive but correct: why E2Es are not the answer to coverage gaps.
The Testing Trophy — Kent C. Dodds
kentcdodds.com
A modern variant emphasising integration over unit. Worth weighing against the pyramid.
Types of Automation Testing
Concept
Automation isn't one thing — it's a toolbox. Knowing which type fits the question saves weeks of misdirected effort.
Example — picking the type
- Unit — single function/class, no I/O. JUnit, Jest, NUnit, pytest.
- Integration / Component — multiple units together (service + DB, component + props). Spring Test, Testcontainers, RTL.
- Contract — the agreement between two services. Pact, Spring Cloud Contract.
- API / Service — black-box HTTP/gRPC against a running service. RestAssured, Playwright API, Postman/Newman.
- End-to-End (UI) — full stack from browser to DB. Playwright, Cypress, Selenium.
- Visual / Snapshot — pixel diff or DOM snapshot to catch UI regressions. Percy, Chromatic, Playwright snapshots.
- Accessibility — WCAG compliance, screen-reader behaviour. axe-core, Pa11y.
- Performance / Load — throughput and latency under load. k6, JMeter, Gatling.
- Security — known vulns, auth bypasses, OWASP Top 10. ZAP, Burp, Snyk.
- Mutation — measures whether your tests would actually catch bugs. Stryker, PIT.
Question to answer Best test type ──────────────────────────────────────── ───────────────── "Does this discount calc round correctly?" Unit "Does the order service write to the DB?" Integration "Does v2 of /orders break the iOS app?" Contract "Does POST /orders return 422 on bad SKU?" API "Can a user actually buy something?" E2E "Did the homepage layout shift?" Visual "Can a screen reader complete checkout?" Accessibility "Does it hold up under 5k req/s?" Performance / LoadExercise
- Take 5 recent bugs in your tracker. Match each to the type of test that would have caught it earliest.
- Identify one type your team isn't doing at all. Sketch the smallest first investment (one tool, one test).
- Run an accessibility scan (axe-core or Lighthouse) on a page you own. Triage the top 3 findings.
Test Automation University
testautomationu.applitools.com — Free
Free courses on every test type by recognised practitioners. Pick one type per month.
Ministry of Testing — Dojo
ministryoftesting.com
The largest free library of testing articles, lessons, and pattern catalogues.
Awesome Test Automation
github.com/atinfo
Curated list of frameworks for every type and language. Useful when scoping a new effort.
Non-Functional Testing
Concept
Functional tests answer "does the feature work?". Non-functional tests answer "does it work well enough?" — under load, securely, accessibly, on the right browsers, fast enough. Easy to skip, expensive to skip.
Example — a k6 load test
- Performance — single-user response time. P95 latency, time-to-first-byte. Tools: Lighthouse, WebPageTest.
- Load — system under expected traffic. Throughput, error rate at target RPS. Tools: k6, JMeter, Locust.
- Stress — push past expected load to find the breaking point. Where does it fail, and how gracefully?
- Soak / Endurance — sustained load over hours/days to find leaks and slow degradations.
- Security — auth bypass, SQL injection, XSS, IDOR, dependency vulnerabilities.
- Accessibility — WCAG conformance, keyboard nav, screen-reader paths.
- Compatibility — supported browsers, OSes, screen sizes, locales.
- Usability — qualitative; harder to automate, but heuristics and scripted task observations help.
- Reliability / Resilience — chaos tests: kill a pod, drop a connection, verify graceful recovery.
// load.js — k6 import http from 'k6/http'; import { check, sleep } from 'k6'; export const options = { vus: 200, // concurrent users duration: '5m', thresholds: { http_req_duration: ['p(95)<500'], // SLO http_req_failed: ['rate<0.01'], }, }; export default function () { const r = http.get('https://api.example.com/orders/42'); check(r, { '200 OK': r => r.status === 200 }); sleep(1); }Exercise
- Pick one critical endpoint. Write down its SLO (latency target, error budget, target RPS).
- Run a 1-minute k6 test against it locally. Read the P95 line.
- Run a Lighthouse audit on a page you own. Capture Performance, Accessibility, Best Practices, SEO scores.
- Run an axe-core scan; file the top 3 a11y issues as tickets.
k6 Documentation
k6.io
Modern, scriptable load tester. JS API, runs from CLI or CI. Best on-ramp into perf testing.
web.dev — Performance
web.dev
Google's primer on Core Web Vitals, Lighthouse, and the metrics that matter.
axe-core / axe DevTools
deque.com
The de-facto accessibility testing engine. Plugs into Playwright, Cypress, browsers, CI.
OWASP Top 10
owasp.org
The ten security risks every web tester should be able to recognise.
API Automation vs Front-end Automation
Concept
Both run automated checks against a real running system, but they target different layers and answer different questions.
API automation talks HTTP/gRPC directly. No browser, no rendering, no JavaScript. Tests run in milliseconds, are stable (no DOM to wait for), parallelise trivially, and isolate the contract from the UI. The right place to assert business rules, error paths, edge cases, auth, and performance.
Front-end automation drives a real browser. It's the only layer that proves users can actually use the thing — that buttons are clickable, that the routing works, that JavaScript loads, that the third-party script didn't break checkout. It's slower, flakier, and more expensive per test.
Decision rule: if the question is "does the backend behave correctly?", use API. If it's "does the UI render and bind correctly?", use front-end. Don't assert business rules through 5 page navigations when one POST proves the same thing.
Example — same rule, two layers
API automation talks HTTP/gRPC directly. No browser, no rendering, no JavaScript. Tests run in milliseconds, are stable (no DOM to wait for), parallelise trivially, and isolate the contract from the UI. The right place to assert business rules, error paths, edge cases, auth, and performance.
Front-end automation drives a real browser. It's the only layer that proves users can actually use the thing — that buttons are clickable, that the routing works, that JavaScript loads, that the third-party script didn't break checkout. It's slower, flakier, and more expensive per test.
Decision rule: if the question is "does the backend behave correctly?", use API. If it's "does the UI render and bind correctly?", use front-end. Don't assert business rules through 5 page navigations when one POST proves the same thing.
// API test — proves the rule, runs in 80ms test('cannot order more than stock', async ({ request }) => { const r = await request.post('/orders', { data: { sku: 'X1', qty: 9999 } }); expect(r.status()).toBe(422); expect((await r.json()).code).toBe('OUT_OF_STOCK'); }); // E2E test — proves the user sees a clear error, runs in 8s test('out-of-stock toast appears', async ({ page }) => { await page.goto('/product/X1'); await page.fill('#qty', '9999'); await page.click('#buy'); await expect(page.locator('.toast--error')).toBeVisible(); }); // Same rule, two angles — and you don't need 50 E2Es to cover qty edge casesExercise
- Pick 5 E2E tests in your suite. Mark which ones could be replaced or shortened by API tests.
- Convert one E2E business-rule check into an API test. Measure the speed difference.
- Identify the smallest set of E2E tests that prove "the UI itself works" — that's your real E2E budget.
Testing Strategies in a Microservice Architecture
martinfowler.com
Clearest treatment of where API/contract/E2E tests fit relative to each other.
Playwright — API Testing
playwright.dev
A single tool for both API and UI tests; useful for hybrid flows (API setup, UI assertion).
Pact — Contract Testing
docs.pact.io
When teams own services on both sides of a contract, contract tests beat both API and E2E.
Why End-to-End Automated Tests Matter
Concept
Lower layers of the pyramid each test a slice. E2E is the only layer that tests the system the way a user actually meets it — browser + frontend + APIs + database + third-party integrations + auth + CDN, all together, as a single behaviour. That's a class of bugs nothing else can catch:
Example — what to E2E and what not to
- Wiring — a perfectly tested API and a perfectly tested UI that don't talk to each other correctly.
- Cross-system flow — login redirects, OAuth handoffs, payment iframes, post-checkout email triggers.
- Environment drift — a misconfigured CDN cache, a wrong CORS origin, an env var that's missing in staging.
- Critical-path regressions — "can a customer pay us?" is the question only an E2E answers honestly.
- Cover the journeys that, if broken, would refund customers or get you on a Slack incident channel. Not every screen.
- Make them ruthlessly stable: clean test data, isolated accounts, stubbed third parties, retries on flakes, screenshots + traces on failure.
- Run them in CI, gate releases on them, surface failures with auto-published reports.
YES — automate as E2E ✓ Sign up → verify email → first login ✓ Add to cart → checkout → payment success → order confirmation ✓ Admin creates a tenant → user from that tenant logs in ✓ Password reset round-trip NO — push these down ✗ Every form-validation message on every field ✗ Every error code from /api/orders ✗ Pixel-perfect rendering of a marketing page ✗ Discount math on every coupon code → Validation errors → component test → API error codes → API test → Pixel diffs → visual snapshot test → Discount math → unit testExercise
- List your product's "if this breaks we lose money or trust" flows. That's your E2E budget.
- For each flow, write down what makes it flaky today, and one specific stabilisation you'd apply.
- Configure your E2E job to capture trace + screenshot on failure and upload as a CI artifact.
Playwright Best Practices
playwright.dev
Modern playbook for stable E2E: locators, isolation, fixtures, parallel.
Cypress Best Practices
docs.cypress.io
Same lessons from a different ecosystem — applicable across tools.
Flaky Tests at Google & How We Mitigate Them
Google Testing Blog
How a team running millions of tests treats flakes. Hard-won wisdom.
01 · Front-end Automation
11 topics
Framework Awareness — Selenium vs Playwright vs Cypress
Concept
Selenium — W3C-standard, every language, every browser. Mature, the safest bet for enterprise legacy stacks. You bolt on waits, parallelism, and reporting.
Playwright — Microsoft, 2020. Auto-waiting, network interception, tracing, parallel by default. Modern web apps. JS/TS first, also Python/Java/.NET.
Cypress — runs inside the browser, beautiful DX, time-travel debugger. Same-origin limits historically; opinionated. JS/TS only.
Pick by context: existing Selenium suite + Java team → stay; greenfield modern app → Playwright; small team that wants the fastest "first test running" curve → Cypress.
Example — same login
Playwright — Microsoft, 2020. Auto-waiting, network interception, tracing, parallel by default. Modern web apps. JS/TS first, also Python/Java/.NET.
Cypress — runs inside the browser, beautiful DX, time-travel debugger. Same-origin limits historically; opinionated. JS/TS only.
Pick by context: existing Selenium suite + Java team → stay; greenfield modern app → Playwright; small team that wants the fastest "first test running" curve → Cypress.
// Playwright await page.goto('/login'); await page.fill('#user', 'admin'); await page.click('button[type=submit]'); await expect(page).toHaveURL(/dashboard/); // Selenium (Java) driver.get("/login"); driver.findElement(By.id("user")).sendKeys("admin"); driver.findElement(By.cssSelector("button[type=submit]")).click(); new WebDriverWait(driver, Duration.ofSeconds(5)) .until(ExpectedConditions.urlContains("/dashboard")); // Cypress cy.visit('/login'); cy.get('#user').type('admin'); cy.get('button[type=submit]').click(); cy.url().should('include', '/dashboard');Exercise
- Implement the same login test in two of the three. Time the run, count the lines.
- Note what each one does automatically that the other doesn't.
- Write a 1-paragraph rationale for which you'd choose for your current product.
Playwright — Getting Started
playwright.dev
Install, first test, project layout.
Selenium WebDriver Docs
selenium.dev
Canonical reference across language bindings.
Playwright vs Selenium — Side-by-side
browserstack.com
Honest architectural comparison and trade-offs.
Handling Web Elements
Concept
A locator is the address used to find an element. Order of preference: role/test-id → id → name → CSS → XPath. Modern tools push you toward role-based locators (
Example
getByRole('button', { name: 'Save' })) — they survive refactors better than CSS selectors. Once located, an element exposes actions (click, fill, selectOption) and queries (textContent, getAttribute, isVisible). Native browser dialogs need explicit handling; iframes and shadow DOM need scoping (covered later in this module).
// Modern locators (preferred) page.getByRole('button', { name: 'Save' }).click(); page.getByLabel('Email').fill('a@x'); page.getByTestId('cart-count'); // CSS — readable, fast page.locator('input[name="email"]'); // XPath — when CSS can't reach (text matching) page.locator('//button[normalize-space()="Save changes"]'); // Validate expect(await page.locator('#total').textContent()).toBe('$42.00'); expect(await page.locator('#submit').isEnabled()).toBe(true);Exercise
- On the-internet.herokuapp.com: automate a checkbox, a dropdown, a JS alert.
- Find the same element via id, css, xpath, and getByRole — compare brittleness.
- Extract a table's row count and assert it equals 4.
Playwright Locators Guide
playwright.dev
Modern strategies with getByRole/getByText. Best-practice patterns.
CSS Selectors Reference
MDN
Every CSS selector with live examples.
XPath Cheatsheet
devhints.io
One-page reference for every axis, function, predicate.
Synchronization & Waits
Concept
Tests fail when the script runs faster than the UI renders. Wrong fix:
Example
Thread.sleep(5000) — slow AND flaky. Right fix: an explicit wait that polls a condition until satisfied or until a timeout. Playwright auto-waits on every action; in Selenium you wrap calls in WebDriverWait. Implicit waits exist but mix poorly with explicit waits — pick one strategy and stick to it.
// BAD Thread.sleep(5000); // GOOD — Selenium new WebDriverWait(driver, Duration.ofSeconds(10)) .until(ExpectedConditions.visibilityOfElementLocated(By.id("result"))); // Playwright auto-waits await expect(page.locator('#result')).toBeVisible();Exercise
- On the-internet.herokuapp.com/dynamic_loading: solve once with sleep, once with explicit wait. Run each 10x — count flakes.
- Set a deliberately short timeout; read the failure message.
- Find one Thread.sleep in your codebase; replace it with a wait and write a 1-line PR description.
Selenium Waits — Official
selenium.dev
Implicit/explicit/fluent with code samples.
Playwright Auto-Waiting
playwright.dev
Exact actionability checks. Read this before writing manual waits.
Selenium Waits Explained
YouTube / Naveen AutomationLabs
Walk-through with anti-pattern examples.
Exceptions & Debugging
Concept
Three exceptions you'll meet week one: NoSuchElement (locator wrong or element not yet rendered), StaleElementReference (DOM re-rendered after you grabbed the reference — re-query), Timeout (wait expired — element really isn't appearing or timeout too short). Read stack traces top-down for the failing line. Verify locators in DevTools Console (
Example — screenshot & trace on failure
$$('css') or $x('xpath')) before blaming the framework. Always capture a screenshot on failure — and a Playwright trace if you can.
// playwright.config.ts use: { screenshot: 'only-on-failure', trace: 'retain-on-failure', video: 'retain-on-failure', } // Selenium — manual hook try { test.run(); } catch (Throwable t) { File shot = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE); Files.copy(shot.toPath(), Paths.get("failure.png")); throw t; }Exercise
- Trigger each of the three exceptions deliberately, then fix each one.
- Add screenshot-on-failure to your runner config; verify a deliberate failure produces one.
- In DevTools Console, validate one CSS and one XPath selector live.
Selenium — Common Errors
selenium.dev
Official catalogue of WebDriver exceptions, causes, fixes.
Playwright Trace Viewer
playwright.dev
Replay every step with DOM snapshots, network, console. Biggest single debug accelerator.
Chrome DevTools Docs
developer.chrome.com
Master Elements/Console/Network — pays back forever.
Assertions
Concept
An assertion is the line that decides pass/fail. Without it, your test only proves the code didn't crash. Assert presence, state (text/attribute/enabled/visible), and navigation (URL/title). Always include a meaningful message — "cart should show 3 items after add" beats "expected 3 got 0". One assertion per behaviour; group related field checks with soft assertions.
Example
// Playwright — auto-retrying matchers await expect(page.locator('#cart-count'), 'cart should show 3 items after add').toHaveText('3'); await expect(page).toHaveURL(/\/checkout/); // JUnit (Java) assertEquals("3", cart.getCount(), "cart should show 3 items after add");Exercise
- Add assertions for: title, URL, an element's text, an attribute.
- Force each to fail; rewrite any vague messages.
- Replace one if/else+log block with an explicit assertion.
Playwright Assertions
playwright.dev
Auto-retrying matchers — replaces flaky
expect(await ...) patterns.JUnit 5 Assertions Guide
junit.org
Every assertion method including
assertAll for soft assertions.Practical Test Pyramid
martinfowler.com
Why one good assertion beats five sloppy ones.
Page Object Model — Basics
Concept
A test should read top-to-bottom like the user story. Hide locators and low-level interactions behind Page Objects — one class per page or component, exposing methods like
Example
login(user, pass) instead of findElement. When the UI changes, you fix one file. Simplest split: locators (private), actions (public methods), tests (assertions and orchestration only).
// pages/LoginPage.ts export class LoginPage { constructor(private page: Page) {} user = () => this.page.getByLabel('Email'); pass = () => this.page.getByLabel('Password'); submit = () => this.page.getByRole('button', { name: 'Sign in' }); async login(u: string, p: string) { await this.user().fill(u); await this.pass().fill(p); await this.submit().click(); } } // tests/login.spec.ts — only intent test('valid login', async ({ page }) => { const login = new LoginPage(page); await page.goto('/login'); await login.login('admin', 'pw'); await expect(page).toHaveURL(/dashboard/); });Exercise
- Refactor any 3-test suite into a Page Object.
- Change a selector in the page object only; verify all 3 tests still pass.
- Lift two reusable methods into a
BasePage.
Page Object — Martin Fowler
martinfowler.com
Original definition. Concise, definitive.
Playwright POM Pattern
playwright.dev
Modern POM in TS with fixtures. Copy-pasteable.
Selenium — Page Object Models
selenium.dev
Java & Python examples.
Dynamic XPath Strategies
Concept
When IDs and CSS classes are auto-generated (
Example
id="x_8c7f23") — common in Angular, React, generated dashboards — you need locators that target structure or content, not volatile attributes. Useful XPath axes and functions: contains(), starts-with(), text(), normalize-space(), sibling/ancestor axes. Goal: a locator anchored on something the developer is unlikely to change — a label, a heading, a stable test-id — and walked from there.
// Volatile id — avoid //div[@id="row_8c7f23"]/button // Anchor on label, walk to control //label[normalize-space()="Email"]/following::input[1] // Cell by row content //tr[td[normalize-space()="Ada"]]/td[contains(@class,"actions")]//button[@aria-label="Edit"] // Data attributes the dev team commits to //*[@data-testid="cart-checkout-btn"]Exercise
- On a real app, find an element with no stable id. Build an XPath anchored on its label.
- Build an XPath that finds a button by row's text content (table cells).
- Negotiate
data-testidon the 3 most-tested components in your app.
XPath Cheatsheet
devhints.io
One page, every axis and function.
XPath 3.1 — W3C Spec
w3.org
For when the cheatsheet runs out. Authoritative.
Making UI Tests Resilient to Change
kentcdodds.com
Why test-ids and role-based queries beat XPath gymnastics — sharpens judgment.
Chrome DevTools Protocol (CDP)
Concept
CDP is the wire protocol that DevTools speaks to Chrome. Playwright is built on it directly; Selenium 4 exposes a
Example — intercept & mock a call
CDP session. With CDP you can intercept network calls, mock responses, throttle bandwidth, emulate devices, capture console logs, and read/write cookies — things WebDriver alone can't do. Practical SDET wins: stub a flaky third-party API, simulate offline mode, capture a HAR for a perf bug.
// Playwright — first-class await page.route('**/api/feature-flags', route => route.fulfill({ json: { newCheckout: true } }) ); // Network throttling const client = await page.context().newCDPSession(page); await client.send('Network.emulateNetworkConditions', { offline: false, downloadThroughput: 50_000, uploadThroughput: 20_000, latency: 500, });Exercise
- Mock a single API response in a test; verify the UI renders the mocked state.
- Throttle network to "Slow 3G" and run a critical flow; observe what breaks.
- Capture all console errors during a test; fail if any appear.
Chrome DevTools Protocol
chromedevtools.github.io
The full protocol reference. Domain-by-domain.
Playwright — Network
playwright.dev
Routing, mocking, HAR recording — the bread & butter of CDP for testers.
Selenium 4 + CDP
selenium.dev
How to use CDP from a Selenium 4 test.
Advanced Waits & Retry Management
Concept
Beyond "wait for visible": fluent waits with custom polling intervals and ignored exceptions; condition composition ("either A is visible OR B is gone"); retries at the right level. Retry the network call, not the whole test — a test that needs 3 retries to pass is hiding a real bug. Playwright's auto-retrying matchers cover most cases. For the rest, write a tiny
Example
waitFor(condition, opts) helper rather than scattering ad-hoc loops.
// Custom wait helper async function waitFor<T>( fn: () => Promise<T | null>, { timeout = 10_000, interval = 200 } = {} ): Promise<T> { const deadline = Date.now() + timeout; while (Date.now() < deadline) { const v = await fn(); if (v) return v; await new Promise(r => setTimeout(r, interval)); } throw new Error('waitFor timed out'); } // Retry at the right level — the API call, not the whole test await retry(() => api.getOrder(id), { times: 3, backoffMs: 500 });Exercise
- Write a
waitForhelper. Replace 3 ad-hoc polling loops with it. - Find a test that retries the whole flow on flake; move the retry to the actual unstable call.
- Quarantine 1 known-flaky test in a separate suite; track it until fixed.
Selenium FluentWait
selenium.dev
Custom polling and exception-ignore configuration.
Playwright — Retries
playwright.dev
Retry strategy at the test level, plus how to detect & quarantine flakes.
Flaky Tests at Google
Google Testing Blog
Flake-handling at scale. The right level to retry, the wrong levels to.
Shadow DOM & iFrames
Concept
iFrames embed a separate document; selectors from the parent don't reach inside. Switch context first (
Example
frame_locator in Playwright, switchTo().frame() in Selenium). Shadow DOM encapsulates a component's DOM (used by web components, payment iframes, design systems); legacy CSS can't pierce it. Playwright pierces shadow DOM by default; Selenium 4 added getShadowRoot(). Common pain points: Stripe Elements, OneTrust banners, Salesforce Lightning components.
// iFrame — Playwright const stripe = page.frameLocator('iframe[name^="__privateStripe"]'); await stripe.getByLabel('Card number').fill('4242424242424242'); // Shadow DOM — Playwright pierces by default await page.locator('my-button >> button').click(); // Selenium 4 WebElement host = driver.findElement(By.tagName("my-button")); SearchContext shadow = host.getShadowRoot(); shadow.findElement(By.cssSelector("button")).click();Exercise
- Automate a Stripe Elements card form using frame locators (use Stripe's test card 4242…).
- Find a shadow-DOM component in your app; build a stable selector.
- Dismiss a cookie banner that lives in a shadow root.
Playwright — Frames
playwright.dev
frameLocator, nested frames, and how shadow piercing works.
Using Shadow DOM — MDN
developer.mozilla.org
Conceptual primer. Helps you reason about why selectors fail.
Selenium 4 — Shadow DOM
selenium.dev
getShadowRoot API and limitations.
Multi-Framework Exposure — Selenium / Playwright / Cypress
Concept
Knowing more than one framework keeps your judgment honest and your career flexible. Each tool has a "native" idiom — fighting it produces brittle code. Selenium's strength is portability across languages and grids. Playwright's strength is auto-waiting, tracing, parallel by default. Cypress's strength is the in-browser DX and time-travel debugger; weakness is the runtime model (no real multi-tab, historically same-origin). When you read a job description or evaluate a tool change, you'll trade off these axes — having shipped tests in two of the three makes that conversation real.
Example — pick the idiom that fits
Question Best fit ──────────────────────────────────────── ──────────── Java team, Selenium Grid in place Selenium Modern web app, want fast onboarding Playwright Pure FE team, component + e2e in one DX Cypress Need cross-browser real-device cloud Selenium / Playwright (BrowserStack/Sauce) Need auto-waits + tracing out of box PlaywrightExercise
- Port one Playwright test to Cypress (or vice versa). Note what each does naturally vs awkwardly.
- Read each tool's "best practices" doc — make a 1-page cheat sheet of differences.
- Write a 1-paragraph tool recommendation for a hypothetical greenfield project.
Playwright Best Practices
playwright.dev
The opinionated Playwright way.
Cypress Best Practices
docs.cypress.io
Same lessons, Cypress idioms.
Selenium Test Practices
selenium.dev
Discouraged patterns and recommended structure.
02 · Programming
6 topics
OOP Basics
Concept
An object bundles state (fields) with behaviour (methods); a class is the blueprint, an instance is the thing you build from it. The constructor initialises new instances. Access modifiers (
Example
public, private, protected) declare who can touch what — keep fields private and expose only the operations callers need (encapsulation). In test automation, every Page Object is a class: locators stay private, actions are public, the constructor takes the page/driver.
public class User { private final String email; private String password; private boolean active = true; public User(String email, String password) { this.email = email; this.password = password; } public String getEmail() { return email; } public void deactivate() { this.active = false; } }Exercise
- Build a
TestUserclass with email, role,fullName(). - Make all fields private; expose only what tests need.
- Instantiate two users; pass them into a login test.
Java OOP Concepts — Oracle
docs.oracle.com
Plain-language tutorial on objects, classes, inheritance, interfaces.
TypeScript Classes Handbook
typescriptlang.org
Modern OOP in TS with constructor shorthand, access modifiers, generics.
Design Patterns Catalog
refactoring.guru
Visual, language-agnostic. Read OOP fundamentals before patterns.
Inheritance & Polymorphism
Concept
Inheritance shares behaviour: a
Example
BasePage with open(), waitForLoad(), screenshot() can be subclassed by every page. Polymorphism means a method called on a base type dispatches to subclass-specific behaviour. Abstract classes share both behaviour and contract; interfaces share contract only. Rule: extend a class to reuse code; implement an interface to share a shape. Modern advice: favour composition over inheritance — deep hierarchies become hard to change.
public abstract class BasePage { protected WebDriver driver; protected BasePage(WebDriver d) { this.driver = d; } public abstract String url(); public void open() { driver.get(url()); } } public class LoginPage extends BasePage { public LoginPage(WebDriver d) { super(d); } @Override public String url() { return "/login"; } }Exercise
- Define a
BasePagewithopen()andtitle(); subclass twice. - Override
title()in each; assert per page. - Try the same with an interface — note where reuse breaks.
Java — Inheritance & Interfaces
docs.oracle.com
Official tutorial: extends, implements, abstract classes, overriding.
Inheritance vs Composition
baeldung.com
Concrete examples for "favour composition" — and when inheritance still wins.
Template Method Pattern
refactoring.guru
The pattern most BasePage hierarchies are quietly implementing.
Error Handling
Concept
try/catch/finally separates the happy path from recovery. Catch only what you can meaningfully handle — silent catch (Exception e) {} is one of the worst bugs you can ship. Throw typed exceptions with messages that name the failed contract. Distinguish expected failures (network timeout — retry) from unexpected ones (NullPointer in your test — fail loud).
try { api.placeOrder(order); } catch (TimeoutException e) { api.placeOrder(order); // expected → retry once } catch (PaymentDeclinedException e) { assertEquals("INSUFFICIENT_FUNDS", e.getCode()); } finally { cleanup.removeOrder(order.getId()); }Exercise
- Wrap a flaky API call in a single retry; log the first failure.
- Define a
TestDataMissingExceptionwith a useful message. - Find one empty
catchin a real codebase; fix it.
Java Exceptions Tutorial
docs.oracle.com
Checked vs unchecked, try-with-resources, hierarchy.
Google JS Style — Exceptions
google.github.io/styleguide
Cross-language wisdom: catch narrow, fail loud, message clearly.
Replace Throw with Notification — Fowler
martinfowler.com
When NOT to use exceptions for control flow.
Data Structures
Concept
Pick the structure that fits the access pattern. List/Array: ordered, index access — test data, table rows. Map/Dictionary: key-value lookup — config, headers, env vars. Set: uniqueness — generated emails, seen IDs. The wrong structure forces you to write loops the language already has built in. Prefer immutable collections for fixtures.
Example
List<User> users = List.of(new User("a@x"), new User("b@x")); Map<String, String> baseUrls = Map.of( "dev", "https://dev.api", "prod", "https://api"); Set<String> seen = new HashSet<>();Exercise
- Build a
TestDataProviderreturning a list of 5 users. - Replace an if/else env-URL block with a Map lookup.
- Use a Set to detect duplicates in generated emails.
Java Collections Trail
docs.oracle.com
List, Set, Map, Queue and their implementations.
Big-O Cheat Sheet
bigocheatsheet.com
Time/space complexity for every common structure.
Data Structures — Easy to Advanced
YouTube / freeCodeCamp
8-hour reference. Jump to the structure you need.
Working with Libraries & Packages
Concept
Each ecosystem has a manifest (
Example
pom.xml, package.json, *.csproj, requirements.txt) and a lockfile that pins exact versions for reproducible builds. Semantic versioning: MAJOR.MINOR.PATCH — major may break, minor adds, patch fixes. Never hand-edit the lockfile. In a shared repo, treat dependency upgrades as deliberate work — read changelogs, run the suite, commit separately.
# npm npm install -D @playwright/test # Maven <dependency> <groupId>com.microsoft.playwright</groupId> <artifactId>playwright</artifactId> <version>1.50.0</version> </dependency> # NuGet dotnet add package Microsoft.PlaywrightExercise
- Add and import 3 libraries; run them.
- Read the lockfile — find one transitive dep you didn't install directly.
- Bump a library by a major version. Read the changelog. Decide.
Semantic Versioning Explained
docs.npmjs.com
MAJOR.MINOR.PATCH and version ranges.
Maven Getting Started
maven.apache.org
Java dependency management — pom, scopes, repositories.
Dependency Management Best Practices
snyk.io
Lockfiles, vuln scanning, pinning vs floating trade-offs.
Code Organization
Concept
A test repo's structure should answer "where would a new teammate look?" without a tour. Common layout:
Example
tests/, pages/, helpers/ (or utils/), fixtures/, config/. Functions short (~30 lines), names self-describing (completeCheckout() not doStuff()), one responsibility per file. If a comment explains what, rename instead — comments earn their place explaining why.
my-tests/ ├─ tests/ │ ├─ login.spec.ts │ └─ checkout.spec.ts ├─ pages/ │ ├─ BasePage.ts │ ├─ LoginPage.ts │ └─ CheckoutPage.ts ├─ fixtures/ │ └─ users.json ├─ utils/ │ └─ apiClient.ts ├─ playwright.config.ts └─ package.jsonExercise
- Reorganise a flat
scripts/folder into the layout above. - Find a 60-line function; split into 3 helpers.
- Delete every comment that just restates the code.
Clean Code — JavaScript
github.com/ryanmcdermott
Robert Martin's principles applied to JS, before/after.
Google Style Guides
google.github.io/styleguide
One source of truth per language.
Naming Things — Kevlin Henney
YouTube
A talk that will change how you name. Worth the hour.
03 · API Automation
10 topics
Bridge: Postman to Code
Concept
A Postman request and a code-based request hit the same endpoint with the same payload — only the runner differs. Every call has 4 movable parts: method + URL, headers, body, auth. Postman's Code button (Ctrl+Alt+C) generates a snippet in any language. Once in code, parse JSON and filter to the fields you care about —
Example
response.json().total beats dumping the entire body.
// JavaScript (fetch) const res = await fetch('https://api/orders/42', { headers: { Authorization: 'Bearer ' + token } }); const data = await res.json(); console.log(data.total, data.items.length); // Java (RestAssured) Response r = given().header("Authorization", "Bearer " + token) .when().get("/orders/42"); System.out.println(r.jsonPath().getDouble("total"));Exercise
- Save a Postman request; convert via Code button. Run it.
- Print only 3 specific fields from the response.
- Add one dynamic header (e.g., a generated request ID).
Postman — Generate Code Snippets
learning.postman.com
Code button in 20+ languages.
REST Assured
rest-assured.io
The Java DSL for HTTP testing.
Fetch API — MDN
developer.mozilla.org
Browser/Node standard for HTTP.
Writing Your First API Test
Concept
API tests follow the same Arrange → Act → Assert shape as UI. For GET, assert status and content. For POST, send a body and assert the resource exists. Chaining means feeding output from one call into the next: create user → grab id → use it on a subsequent
Example
/orders POST. Assert against the contract (status + key fields), not the whole response — your test should survive harmless additions.
test('create then read user', async ({ request }) => { const created = await request.post('/users', { data: { name: 'Ada', role: 'admin' } }); expect(created.status()).toBe(201); const { id } = await created.json(); const got = await request.get(`/users/${id}`); expect(got.status()).toBe(200); expect((await got.json()).name).toBe('Ada'); });Exercise
- Against reqres.in: GET asserting status + 1 field.
- POST that creates and asserts non-null id.
- Chain POST → GET to prove the resource is retrievable.
Playwright API Testing
playwright.dev
First-class HTTP fixture — auth, files, parallel.
Reqres.in — Practice API
reqres.in
Free hosted endpoints for first tests.
HTTP Status Codes Reference
restfulapi.net
200 vs 201 vs 204, 400 vs 422, etc.
Making Tests Reusable
Concept
Hardcoding URLs, headers, and bodies into individual tests is a maintenance trap. Lift them out: base URL → config (per env), headers → shared client (auth applied once), bodies → JSON files. For data-driven cases, read fixtures from JSON or CSV and run the same test for each row. When the API renames a header or rotates a token, you fix one place.
Example
// playwright.config.ts use: { baseURL: process.env.API_URL ?? 'https://dev.api', extraHTTPHeaders: { 'X-Tenant': 'qa' } } // fixtures/users.csv email,role,expectedStatus ada@x,admin,200 guest@x,guest,403Exercise
- Move base URL out of every test into a single config.
- Build a
requestwrapper that auto-attaches auth. - Externalise one POST body to JSON; load it from the test.
- Drive the same test from a 3-row CSV.
Parametrize Tests — Playwright
playwright.dev
Data-driven, env-driven, projects.
12-Factor App — Config
12factor.net
Why config belongs in env vars, not code.
csv-parse (Node)
npmjs.com
Battle-tested CSV reader. Sync & streaming.
Working Without a Backend (Mocks)
Concept
When the backend isn't ready, or you need to simulate failures (500, timeout, malformed payload), use a mock server. Postman ships one; WireMock (Java) and MSW (Node, browser) are local alternatives. Key skill: design tests that pass against the real server and the mock — your mock must match the real contract, not invent its own.
Example
# 1) Postman: Collection → ⋯ → Mock collection # 2) Get a URL like https://<id>.mock.pstmn.io # 3) Define examples (200 success, 500 error) baseURL: process.env.USE_MOCK === '1' ? 'https://abcd.mock.pstmn.io' : 'https://api.dev';Exercise
- Spin up a Postman mock for
GET /users/{id}with 200 + 404 examples. - Write tests that pass against the mock for both cases.
- Add a 500 example; verify your test surfaces it cleanly.
Postman Mock Servers
learning.postman.com
Collection → examples → mock URL.
WireMock
wiremock.org
Open-source HTTP mock server for JVM. Stubs, scenarios, fault simulation.
Mock Service Worker (MSW)
mswjs.io
Best-in-class for front-end mocking; works in Node and browser.
Serialisation / Deserialisation
Concept
Serialisation turns objects into JSON; deserialisation parses JSON back into typed objects. Strong-typed deserialisation catches schema drift early — if the response no longer has
Example
userId, your test fails on a missing field, not on a misleading null three lines later. Tools: Jackson/Gson (Java), System.Text.Json (.NET), Pydantic (Python), zod or TypeScript types + runtime validation (Node).
// TypeScript with zod import { z } from 'zod'; const Order = z.object({ id: z.number(), items: z.array(z.object({ sku: z.string(), qty: z.number() })), total: z.number(), }); const raw = await (await request.get('/orders/42')).json(); const order = Order.parse(raw); // throws if shape changed expect(order.items).toHaveLength(3);Exercise
- Define a schema/POJO for a real endpoint; deserialise the response.
- Break the schema deliberately; observe the failure message.
- Test a nested array endpoint — assert against typed sub-fields.
Jackson — Java JSON Library
github.com/FasterXML
The standard for JSON in Java. ObjectMapper, annotations, modules.
zod
zod.dev
TypeScript-first schema validation; ideal for API response checks.
Pydantic
docs.pydantic.dev
Python data validation via type hints. Ergonomic and fast.
Data-Driven & Auth
Concept
Parameterise tests from external data (JSON/CSV/DB) so adding a new case is a row, not a copy-pasted test. Auth in API tests usually means OAuth2: obtain a token at suite setup, refresh when it expires, attach it to every call. Role-based access: run the same test with multiple tokens (admin/user/guest) and assert the differing status codes — that's how you prove authorisation works.
Example
async function getToken(role: string) { const r = await request.post('/oauth/token', { form: { grant_type: 'password', username: users[role].email, password: users[role].password } }); return (await r.json()).access_token; } for (const [role, expected] of [['admin', 200], ['guest', 403]]) { test(`admin endpoint as ${role}`, async ({ request }) => { const token = await getToken(role as string); const r = await request.get('/admin/users', { headers: { Authorization: `Bearer ${token}` } }); expect(r.status()).toBe(expected); }); }Exercise
- Drive a test from a 5-row JSON dataset.
- Implement OAuth2 password-grant token retrieval; cache for the suite.
- Write a role matrix (admin/user/guest × 3 endpoints) and assert correct codes.
OAuth 2.0
oauth.net
Canonical reference. Read the grant types section.
Playwright — Authentication
playwright.dev
Storage state, reuse across tests, multi-role patterns.
OWASP API Security — Broken Authorization
owasp.org
Why role matrices matter. Real exploit examples.
Non-Functional API Testing
Concept
Beyond "does it work": does it work fast enough, under load, concurrently? Response time: assert P95 latency on critical endpoints. Load: target RPS at expected traffic; stress: push to breaking point. Concurrency: hit the same resource from multiple "users" to surface race conditions (double-charges, duplicate orders). k6, Gatling, JMeter, Locust — all good. Run as a separate CI job, not on every PR.
Example
// k6 — concurrent ordering export const options = { scenarios: { burst: { executor: 'shared-iterations', vus: 50, iterations: 50 } } }; export default function () { const r = http.post('https://api/orders', JSON.stringify({ sku: 'X1', qty: 1 }), { headers: { 'Content-Type': 'application/json' } }); check(r, { '201 created': r => r.status === 201 }); }Exercise
- Pick one critical endpoint; write a k6 SLO test (P95 latency at target RPS).
- Fire 50 concurrent POSTs to a "create unique resource" endpoint; assert no duplicates.
- Add a perf job to CI that runs nightly, not per-commit.
k6 — Test Types
k6.io
Smoke, load, stress, spike, soak — when to run each.
Apache JMeter User Manual
jmeter.apache.org
The classic. Heavier than k6 but ubiquitous in enterprise.
Gatling Docs
gatling.io
Scala/Java/Kotlin DSL. Strong reporting and CI integration.
JSON Schema Validation
Concept
A JSON Schema is a contract describing the shape of valid responses. Validating responses against a schema catches drift the moment a backend team accidentally renames or drops a field. Pair with OpenAPI/Swagger: most teams already publish a schema you can reuse.
Example
// Ajv (Node) against a schema generated from OpenAPI import Ajv from 'ajv'; const ajv = new Ajv(); const validate = ajv.compile(orderSchema); const body = await (await request.get('/orders/42')).json(); if (!validate(body)) throw new Error(JSON.stringify(validate.errors));Exercise
- Generate a JSON Schema from your OpenAPI spec (or write one for a small response).
- Add schema validation to one existing test; deliberately break the response shape.
- Compare schema validation to typed deserialisation — when do you want both?
JSON Schema — Getting Started
json-schema.org
Official step-by-step. Start here.
Ajv — JSON Schema Validator
ajv.js.org
The fastest JS validator. Drops into any test runner.
OpenAPI Specification
swagger.io
The contract format your backend team probably already publishes.
API Test Organisation
Concept
As a suite grows, structure it like the API. Common pattern:
Example
tests/api/<resource>/<operation>.spec.ts. A client layer wraps fetch/RestAssured calls per resource (OrdersClient.create(), OrdersClient.get()) so tests express intent. Fixtures handle setup/teardown idempotently. Tag tests by speed/risk so you can run smoke vs full suites independently.
tests/
└─ api/
├─ orders/
│ ├─ create.spec.ts
│ ├─ list.spec.ts
│ └─ delete.spec.ts
└─ users/
└─ login.spec.ts
clients/
├─ OrdersClient.ts
└─ UsersClient.ts
fixtures/
└─ test-tenant.ts
Exercise
- Reorganise a flat
api-tests/folder by resource. - Build one client class for the most-used resource.
- Tag 5 tests as
@smoke; configure a CI job that runs only them on PR.
Playwright Fixtures
playwright.dev
Composable setup/teardown — the cleanest model in any test framework.
Component Tests in Microservices
martinfowler.com
How to organise API/component tests at scale.
Architecting API Tests
thoughtworks.com
Layered structure, client abstraction, contract vs functional separation.
CI/CD Pipeline Integration
Concept
API tests belong in CI: fast, stable, ideal gate. Standard pipeline shape: smoke on every PR (1-2 min), full regression on merge to main, nightly for non-functional. Publish reports as artifacts; surface failures as PR check annotations. Secrets via environment variables, never YAML. Tag-based selection (
Example
--grep @smoke) keeps PR feedback fast.
jobs:
api-smoke:
runs-on: ubuntu-latest
env: { API_URL: ${{ secrets.QA_API_URL }},
TOKEN: ${{ secrets.QA_TOKEN }} }
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npx playwright test --grep @smoke --reporter=junit,html
- if: always()
uses: actions/upload-artifact@v4
with: { name: api-report, path: playwright-report/ }
Exercise
- Add a smoke job that runs on every PR (< 2 min target).
- Add a nightly full-regression job; set up notifications on failure.
- Configure a status badge for the API suite in your README.
Playwright on CI
playwright.dev
Recipes for GitHub Actions, Azure, GitLab, Jenkins.
Using Secrets — GitHub Actions
docs.github.com
Storage, masking, scoping (env/repo/org).
Test Reporter Action
github.com/marketplace
Render JUnit/Mocha/Jest output as PR check annotations.
04 · SQL
14 topics
DDL & Schema Understanding
Concept
DDL (Data Definition Language) creates and alters structure:
Example
CREATE, ALTER, DROP. As an SDET you rarely run these in production but you read them constantly to understand the data shape. Each table holds one entity; columns have explicit types and nullability; every table should have a primary key. Recognise NOT NULL DEFAULT 'x' vs nullable — it changes which test inputs are valid.
CREATE TABLE users ( id BIGINT PRIMARY KEY AUTO_INCREMENT, email VARCHAR(255) NOT NULL UNIQUE, role VARCHAR(32) NOT NULL DEFAULT 'guest', created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); ALTER TABLE users ADD COLUMN active BOOLEAN NOT NULL DEFAULT 1;Exercise
- Spin up SQLite or use DB Fiddle. Create
users+orderswith FKs. - Add a column with ALTER; drop the column.
- Read your project's schema; sketch the ERD on paper.
PostgreSQL — Data Definition
postgresql.org
Most rigorous DDL reference of any major DB.
SQLite CREATE TABLE
sqlite.org
Concise reference. Easiest engine to play with locally.
DB Fiddle
db-fiddle.com
Run schema + queries in MySQL/Postgres/SQLite in browser.
All JOIN Types
Concept
JOINs combine rows from two tables based on a relationship. INNER: only rows with matches in both. LEFT: every row from the left, NULL on right when no match. RIGHT: mirror of LEFT, less common. FULL OUTER: every row from both. CROSS: cartesian — every left × every right (rare). SELF: a table joined to itself (employee → manager).
Example
-- INNER: users who placed orders SELECT u.email, o.id FROM users u INNER JOIN orders o ON o.user_id = u.id; -- LEFT: every user + orders if any SELECT u.email, o.id FROM users u LEFT JOIN orders o ON o.user_id = u.id; -- SELF: each employee with manager's name SELECT e.name, m.name AS manager FROM employees e LEFT JOIN employees m ON e.manager_id = m.id;Exercise
- SQLZoo JOINs tutorial end-to-end.
- Same query 3 ways: INNER, LEFT, RIGHT — compare row counts.
- Find users with NO orders (LEFT JOIN + IS NULL).
SQLZoo — JOIN Tutorial
sqlzoo.net
Interactive lessons. Classic free SQL trainer.
Visual Explanation of SQL JOINs
blog.codinghorror.com
The famous Venn diagrams.
Mode SQL Tutorial — JOINs
mode.com
Worked examples on real-ish data.
Complex WHERE Clauses
Concept
A WHERE clause is boolean algebra.
Example
AND restricts; OR permits. Combine with parentheses for precedence. IN (...) for "any of"; BETWEEN for ranges; LIKE for patterns; IS NULL for nullability (never = NULL). A subquery inside WHERE returns a value or list to filter on.
SELECT * FROM orders WHERE status IN ('paid', 'shipped') AND total BETWEEN 10 AND 100 AND (country = 'US' OR country = 'CA'); SELECT email FROM users WHERE id IN ( SELECT user_id FROM orders WHERE status = 'unpaid' );Exercise
- Write a 3-condition query; rewrite with parentheses to flip meaning.
- Replace an OR chain with IN.
- Solve "products never ordered" using a subquery.
Mode SQL — WHERE
mode.com
Operator-by-operator with examples.
SQL Server Subqueries
sqlservertutorial.net
Where subqueries can appear; how the optimiser treats them.
LeetCode — Top 50 SQL
leetcode.com
Free curated problem set.
Aggregate Functions & GROUP BY
Concept
COUNT, SUM, AVG, MIN, MAX collapse rows. GROUP BY partitions rows first so you get one number per group. Rule: every column in SELECT must be inside an aggregate or in GROUP BY. HAVING filters after grouping; WHERE filters before.
SELECT u.email, COUNT(o.id) AS orders, SUM(o.total) AS spent FROM users u LEFT JOIN orders o ON o.user_id = u.id WHERE o.status = 'paid' GROUP BY u.email HAVING COUNT(o.id) > 5 ORDER BY spent DESC;Exercise
- Compute total revenue per month.
- List products ordered fewer than 3 times.
- Find each customer's largest single order with MAX.
Mode — Aggregates & GROUP BY
mode.com
Best concise treatment of the WHERE/GROUP BY/HAVING order.
PostgreSQL Aggregate Functions
postgresql.org
Full reference, including statistical aggregates.
HackerRank — SQL Practice
hackerrank.com
Free graded problems including aggregation.
Database Connectivity from Code
Concept
To run SQL from a test, you need a driver. Java: JDBC. .NET: ADO.NET. Node:
Example — JDBC
pg, mysql2. Flow: open connection → create statement → execute → read result → close. Always close (try-with-resources, using, or a pool) — leaks exhaust the database.
try (Connection c = DriverManager.getConnection(url, user, pass); PreparedStatement ps = c.prepareStatement("SELECT email FROM users WHERE id = ?")) { ps.setLong(1, 42L); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) System.out.println(rs.getString("email")); } }Exercise
- Connect to local SQLite or Postgres from your test project.
- Run SELECT; read 1 column.
- Write a teardown that deletes the test rows you created.
JDBC Basics — Oracle
docs.oracle.com
Connection, Statement, ResultSet — the four classes that do 95% of the work.
ADO.NET Overview
learn.microsoft.com
Microsoft's official guide for SQL connectivity from .NET.
node-postgres (pg)
node-postgres.com
De facto Postgres driver for Node. Pooling, parametrised queries.
Parameterized Queries
Concept
Never concatenate user input into SQL — that's the #1 cause of SQL injection. Use parameterised queries: the driver sends SQL and parameters separately, so the database never parses data as code. Prepared statements also pre-compile the plan, speeding repeated execution.
Example
// BAD — vulnerable to '; DROP TABLE users;-- Statement s = c.createStatement(); s.execute("SELECT * FROM users WHERE email = '" + email + "'"); // GOOD PreparedStatement ps = c.prepareStatement( "SELECT * FROM users WHERE email = ?"); ps.setString(1, email);Exercise
- Convert one concatenated query to a prepared statement.
- Try injecting
' OR 1=1 --against both versions; observe. - Explain to a teammate why parameter binding stops the attack.
OWASP — SQL Injection
owasp.org
Definitive overview of the attack and defences.
Query Parameterization Cheat Sheet
cheatsheetseries.owasp.org
Right way to bind in Java, .NET, PHP, Ruby, Python.
Bobby Tables — Defenses by Language
bobby-tables.com
One-pager with correct binding syntax everywhere.
Transaction Basics
Concept
A transaction groups statements into one atomic unit — all succeed or all roll back.
Example
BEGIN starts, COMMIT persists, ROLLBACK undoes. Crucial for tests: wrap setup + action + teardown in a transaction and rollback at the end — DB stays clean. Production code uses transactions for ACID guarantees (Atomicity, Consistency, Isolation, Durability).
c.setAutoCommit(false); try { insertUser(c, "ada@x"); insertOrder(c, ada.id, 42.00); runTest(); } finally { c.rollback(); }Exercise
- Wrap inserts in BEGIN/ROLLBACK; confirm rows don't persist.
- Force a failure mid-flow; verify nothing committed.
- Read up on isolation levels; identify your DB's default.
PostgreSQL — Transactions
postgresql.org
Cleanest intro to transactions and isolation.
ACID Properties
wikipedia.org
Surprisingly clear primer.
Write-Ahead Log — Fowler
martinfowler.com
The mechanism that makes COMMIT durable.
Result Set Handling & Schema Navigation
Concept
A ResultSet is a forward-only cursor. The mature pattern: map each row to a typed object (POJO/DTO/record) so the rest of your test reads typed code. For exploration, learn to read an ERD (boxes = tables, lines = FKs, crow's-foot = cardinality). When no ERD exists, query
Example
information_schema.
record UserRow(long id, String email, String role) {} List<UserRow> rows = new ArrayList<>(); while (rs.next()) rows.add(new UserRow( rs.getLong("id"), rs.getString("email"), rs.getString("role"))); -- Discover schema SELECT table_name, column_name, data_type FROM information_schema.columns WHERE table_schema = 'public';Exercise
- Query information_schema; list every table and FK.
- Sketch the ERD on paper from the FK list.
- Write a helper mapping a ResultSet row to a typed record.
Reading ER Diagrams
lucidchart.com
Notation: cardinality, weak entities, relationships.
information_schema — Postgres
postgresql.org
Standard, cross-DB schema introspection.
DBeaver
dbeaver.io
Free, multi-engine GUI. Auto-generates ERDs.
Indexes Awareness
Concept
An index is a separate B-tree structure that lets the DB find rows without scanning the whole table. Primary keys are indexed automatically; FKs often should be. Trade-off: indexes speed up SELECTs but slow INSERTs/UPDATEs/DELETEs. Use
Example
EXPLAIN to see if the planner uses an index.
EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id = 42; -- Seq Scan on orders ... (10 ms) CREATE INDEX idx_orders_user ON orders(user_id); EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id = 42; -- Index Scan ... (0.2 ms)Exercise
- Run EXPLAIN on a query against a 100k-row table; note "seq scan" vs "index scan".
- Add an index; re-run EXPLAIN; observe.
- Insert 1k rows on the indexed table; note the (small) write cost.
Use The Index, Luke!
use-the-index-luke.com
Free online book on indexes for developers. Required reading.
PostgreSQL Indexes
postgresql.org
Index types (B-tree, hash, GIN), partial, expression.
Using EXPLAIN
postgresql.org
Read query plans like a pro.
Window Functions
Concept
Window functions compute a value across a "window" of rows without collapsing the result like GROUP BY does.
Example
ROW_NUMBER(), RANK(), DENSE_RANK(), LEAD(), LAG() over PARTITION BY. Used for: ranking ("top 3 customers per country"), time-series gaps (LEAD/LAG to compare a row to its neighbours), running totals.
-- Top 3 customers by spend per country SELECT * FROM ( SELECT country, email, spent, ROW_NUMBER() OVER (PARTITION BY country ORDER BY spent DESC) AS rn FROM customer_totals ) ranked WHERE rn <= 3;Exercise
- Find each customer's most-recent order using
ROW_NUMBER + PARTITION BY. - Use
LAGto compute time between consecutive orders per user. - Compute a running total of daily revenue.
PostgreSQL Window Functions
postgresql.org
The clearest tutorial in any DB doc.
Mode — Window Functions
mode.com
Worked examples on real-shape datasets.
Window Functions Cheat Sheet
learnsql.com
PDF reference. Pin to your monitor.
Common Table Expressions (CTEs)
Concept
A CTE is a named query you reference later in the same statement (
Example
WITH foo AS (...) SELECT ... FROM foo). They make complex queries readable by naming intermediate steps. Recursive CTEs handle hierarchies (manager chains, category trees) without writing self-joins repeatedly.
WITH recent_orders AS ( SELECT user_id, MAX(created_at) AS last_at FROM orders GROUP BY user_id ) SELECT u.email, r.last_at FROM users u JOIN recent_orders r ON r.user_id = u.id; -- Recursive: manager chain WITH RECURSIVE chain AS ( SELECT id, name, manager_id FROM employees WHERE id = 42 UNION ALL SELECT e.id, e.name, e.manager_id FROM employees e JOIN chain c ON e.id = c.manager_id ) SELECT * FROM chain;Exercise
- Rewrite a nested subquery using a CTE; compare readability.
- Write a recursive CTE for a category tree.
- Chain 3 CTEs to express a multi-step transformation.
PostgreSQL — WITH Queries (CTEs)
postgresql.org
Including recursive examples.
What is a CTE
learnsql.com
Plain-language tutorial with progressive examples.
Modern SQL — WITH
modern-sql.com
Cross-database compatibility notes.
Complex Subqueries (Correlated, EXISTS, IN vs JOIN)
Concept
A correlated subquery references the outer query's row — it runs once per outer row (potentially slow).
Example
EXISTS stops at the first match (often faster than IN over large sets). IN vs JOIN: equivalent for many cases, but JOINs can duplicate rows when the right side has multiples — be aware.
-- Correlated SELECT u.email FROM users u WHERE (SELECT COUNT(*) FROM orders o WHERE o.user_id = u.id) > 5; -- EXISTS — short-circuits SELECT u.email FROM users u WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id AND o.status = 'unpaid');Exercise
- Rewrite an IN(subquery) as EXISTS; compare EXPLAIN plans.
- Find one correlated subquery; rewrite as a JOIN; compare.
- Identify a case where IN and JOIN return different counts (duplicates).
Use The Index, Luke! — Search Patterns
use-the-index-luke.com
How subqueries and EXISTS interact with indexes.
EXISTS vs IN vs JOIN
sqlservercentral.com
Performance comparison with worked examples.
LATERAL Joins
modern-sql.com
When you need correlated-subquery semantics in a JOIN.
Database Testing
Concept
Beyond unit-testing application code, you can test the database itself: data integrity (constraints, NOT NULLs, referential integrity), migrations (do up/down apply cleanly?), seed data (does test data load correctly?), stored procedures (input/output assertions). Testcontainers spins up real DBs in Docker for integration tests — beats in-memory H2 for fidelity.
Example — Testcontainers (Java)
@Container PostgreSQLContainer<?> pg = new PostgreSQLContainer<>("postgres:16"); @Test void migrationsApplyCleanly() { Flyway.configure() .dataSource(pg.getJdbcUrl(), pg.getUsername(), pg.getPassword()) .load() .migrate(); // then assert against the migrated schema }Exercise
- Spin up a Postgres container in a test; run migrations against it.
- Write a test asserting a NOT NULL constraint actually rejects NULLs.
- Add a test that round-trips a stored procedure with sample input.
Testcontainers
testcontainers.org
Real DBs/queues/services for integration tests, in Docker. Java/JS/.NET/Python/Go.
Flyway
flywaydb.org
SQL migrations with versioning. Industry standard.
Evolutionary Database Design
martinfowler.com
Foundational article on testing migrations and DB change.
NoSQL Awareness
Concept
Relational databases enforce schema and ACID. NoSQL trades some of that for scale or flexibility. Categories: document (MongoDB, DynamoDB) — JSON-shaped records, schema-flexible; key-value (Redis, DynamoDB) — fast lookup by key, no joins; columnar (Cassandra) — wide tables with eventual consistency; graph (Neo4j) — relationships are first-class. When to use which: relational for structured records with relationships and integrity; document for evolving schemas and nested data; key-value for caches and sessions; graph for "friend-of-friend" queries.
Example — same data, two stores
-- Relational users(id, email) orders(id, user_id, total) SELECT o.* FROM orders o JOIN users u ON u.id = o.user_id WHERE u.email = 'a@x'; // Document (Mongo) — orders embedded in user { "_id": "u1", "email": "a@x", "orders": [{ "id": "o1", "total": 42 }] } db.users.find({ email: 'a@x' }, { orders: 1 })Exercise
- Run MongoDB locally (Docker). Insert and query a document.
- Model a small domain twice — relational vs document. Compare query patterns.
- Identify one place in your product where NoSQL would simplify, and one where it would hurt.
MongoDB Manual
mongodb.com
The dominant document DB. Strong tutorials.
NoSQL Distilled — Fowler
martinfowler.com
Concise framing of when relational is right and when it isn't.
NoSQL — AWS Overview
aws.amazon.com
Vendor-agnostic enough; surveys the four NoSQL categories.
05 · Code Versioning (Git)
8 topics
Version Control Fundamentals
Concept
Version control records every change so you can rewind, branch alternatives, and collaborate without overwriting each other. Centralized systems (SVN, Perforce) keep one authoritative server. Distributed systems (Git, Mercurial) give every clone the full history — commit offline, sync later. Git is the standard. The basic loop:
Example
clone → edit → add → commit → push / pull.
git clone https://github.com/me/my-tests.git cd my-tests echo "new test" > tests/login.spec.ts git add tests/login.spec.ts git commit -m "add login happy-path test" git push origin main git pullExercise
- Clone any public repo, add a file, commit, push to a fork.
- Inspect
.git/; see commits as files inobjects/. - Compare centralized vs distributed in a 1-paragraph note.
Pro Git — About Version Control
git-scm.com
The free, definitive Git book. Chapters 1-3 cover 80% of daily Git.
Atlassian Git Tutorials
atlassian.com
Visual diagrams and concrete scenarios.
Learn Git Branching
learngitbranching.js.org
Interactive in-browser Git simulator. Builds intuition fast.
Git Basics — Daily Commands
Concept
Configure your identity (
Example
git config user.email) — every commit records it. The staging area sits between "edited" and "committed", letting you commit selected hunks. Commit messages matter: short imperative subject ("fix flaky login wait"), optional body explaining why. Use git log to read history, git diff to see what's about to be committed.
git status git diff git diff --staged git add -p # interactive hunk staging git commit -m "fix flaky login wait" git log --oneline -10 git log --oneline --graph --allExercise
- Make 3 commits with messages that pass the "would I understand this in 6 months?" test.
- Use
git add -pto stage half a file's changes. - Run
git log --graphon a multi-branch repo.
Conventional Commits
conventionalcommits.org
A widely-used format pairing nicely with automated changelogs.
How to Write a Git Commit Message
cbea.ms
The seven rules. Short, opinionated, correct.
Oh Shit, Git!?!
ohshitgit.com
Recovery recipes for the moments after you panic.
Branching & Merging
Concept
A branch is a movable pointer to a commit; creating one is free.
Example
main stays green; work happens on feature branches and merges back. Merge creates a merge commit joining two histories. Rebase rewrites your branch's commits onto the tip of another — cleaner linear history, but never rebase commits you've shared. Conflicts happen when two branches changed the same lines.
git switch -c fix/login-flake
# ... edit, commit ...
git switch main
git pull
git switch fix/login-flake
git rebase main
git switch main
git merge fix/login-flake
Exercise
- Create two branches that edit the same line. Merge → resolve → commit.
- Try the same with rebase. Note when each is more readable.
- Read 5 random merge commits in a real repo; rate the messages.
Pro Git — Branching
git-scm.com
How branches actually work under the hood.
Merging vs Rebasing
atlassian.com
The clearest single piece on the trade-off.
Resolving Merge Conflicts
docs.github.com
CLI and web UI workflows.
Collaboration Basics — Pull Requests
Concept
A fork is your own copy of a repo; you push to your fork and propose changes via a pull request. Within an org, you usually skip the fork and push branches directly. A good PR is small, has a clear title and description, links the ticket, and explains the why. Don't take review feedback personally — it's the system that catches what your tests didn't.
Example
git switch -c feat/cart-pom # ... commits ... git push -u origin feat/cart-pom gh pr create --fill --base main # review comes in... git commit -m "address review: extract helper" git pushExercise
- Fork a small open-source repo, fix a typo, open a PR.
- Read 3 well-reviewed PRs in a real repo; note what made the discussion productive.
- On your next PR, write the description before the code.
Pull Requests — GitHub Docs
docs.github.com
From "create" to advanced — codeowners, drafts, suggestions.
Google Code Review Guidelines
google.github.io/eng-practices
Reviewer and author guides. Healthy review culture.
How to Make Your Code Reviewer Fall in Love
mtlynch.io
Practical guide to authoring PRs that get fast approvals.
Git Workflows — Gitflow vs Trunk-Based
Concept
Gitflow uses long-lived branches:
Example
main (production), develop (integration), feature/*, release/*, hotfix/*. Heavy ceremony; suits versioned/released products. Trunk-based: short-lived feature branches off main, merged within a day or two, with feature flags hiding incomplete work in production. Optimised for continuous deployment. Most modern web teams use trunk-based; large packaged software still uses Gitflow. Pick by release cadence, not fashion.
Trunk-based daily flow:
main ─●─●─●─●─●─●─●─
\ \ \
f1 f2 f3 // each merged within hours
Gitflow:
main ─●───────●───────●─
develop ─●─●─●─●─●─●─●─●─●─
\ \ \ \
f1 f2 f3 release/1.2
Exercise
- Identify which workflow your team uses; document it in 1 paragraph.
- For one feature, sketch how it would flow through both workflows.
- Set up a feature-flag library; gate a small change behind it.
Trunk Based Development
trunkbaseddevelopment.com
The definitive resource. Patterns and trade-offs.
A Successful Git Branching Model (Gitflow)
nvie.com
The original Gitflow article. Author's later note recommends trunk-based for web apps.
Feature Toggles — Fowler
martinfowler.com
The mechanism that makes trunk-based work for incomplete features.
History Management — Revert, Reset, Reflog
Concept
git revert creates a new commit that undoes a previous one (safe; preserves history — use on shared branches). git reset moves the branch pointer (dangerous on shared branches; great for cleaning local work). --soft keeps changes staged; --mixed keeps them unstaged; --hard discards. git reflog records every HEAD move for ~90 days — your safety net when you "lost" commits. Cherry-pick applies one commit from another branch. Squash + rebase consolidates messy WIP commits before merging.
git revert <sha> # safe undo on shared branch git reset --soft HEAD~1 # un-commit, keep changes staged git reset --hard HEAD~1 # nuclear; lose changes git reflog # find "lost" commits git cherry-pick <sha> # apply one commit elsewhere git rebase -i HEAD~5 # squash/reorder last 5Exercise
- Revert a commit on a branch; observe the new "Revert ..." commit.
- Use
git reset --hardto discard local work, then recover with reflog. - Squash 4 WIP commits into 1 with interactive rebase.
Git Reset Demystified
git-scm.com
The clearest explanation of soft/mixed/hard. Stops being scary.
git reflog Reference
git-scm.com
The lifesaver. Recover commits even after a hard reset.
Rewriting History
atlassian.com
Amend, rebase, cherry-pick, reset — when each applies.
PR & Code Review Mastery
Concept
A reviewable PR is small (target < 400 lines), single-purpose, and has a description that lets a reviewer skip reading the diff once before reading it again. As a reviewer: prioritise correctness, design, and tests; tolerate style; never block on bikeshed. As an author: respond to every comment (even with "agreed, fixed in a1b2c3d"); resolve conflicts on your branch, never on main.
Example — strong PR description
## Why Login intermittently fails on CI when the auth service responds slowly. The current test asserts on URL before the redirect completes. ## What - Replace fixed sleep with auto-retrying URL matcher - Add screenshot+trace on failure - Linked: TICKET-1234 ## Test plan - Ran the test 50x locally with throttled network — 0 flakes - CI run: <link>Exercise
- Take your last PR; rewrite the description in the format above.
- Review one open PR in your repo with the lens "what's the smallest blocker?".
- Resolve a merge conflict on a branch end-to-end without using the GitHub UI.
Google — How to Do a Code Review
google.github.io
Reviewer's guide. Concise and battle-tested.
Code Review Best Practices — Palantir
medium.com
What to look for, in priority order. Detailed checklist.
Peer Code Review Best Practices
smartbear.com
Empirical findings: 200-400 LOC per review, 60-90 min, then quality drops.
Tagging & Releases
Concept
A tag is an immutable pointer to a commit, used to mark releases. Annotated tags (
Example
git tag -a v1.2.0 -m "...") carry metadata; lightweight tags are just pointers. Pair with SemVer for human readability. GitHub Releases attach a tag, a changelog, and built artifacts. Automate tag creation in CI on merge to main, with the version derived from conventional commits.
git tag -a v1.2.0 -m "Release 1.2.0 — adds CSV import" git push origin v1.2.0 # GitHub release with notes gh release create v1.2.0 --generate-notes # List by semver git tag --sort=-v:refname | headExercise
- Tag a commit; push the tag; verify it appears on GitHub.
- Create a GitHub Release with auto-generated notes.
- Add a CI step that tags on merge to main using a SemVer tool (e.g., release-please).
Pro Git — Tagging
git-scm.com
Lightweight vs annotated, signing, pushing tags.
Semantic Versioning
semver.org
The contract behind MAJOR.MINOR.PATCH.
release-please
github.com/googleapis
Auto-tag and changelog from conventional commits.
06 · CI / CD
3 topics
Core Concepts — CI vs CD
Concept
Continuous Integration = on every push, build and run tests automatically — broken code caught in minutes, not days. Continuous Delivery = build artifacts always release-ready (a human clicks deploy). Continuous Deployment = the same pipeline ships straight to production if all checks pass. Your tests are the heart of CI; they're what makes the green/red light meaningful. Modern pipelines are YAML files in the repo (
Example
.github/workflows/, azure-pipelines.yml, .gitlab-ci.yml).
# .github/workflows/ci.yml name: ci on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: { node-version: '20' } - run: npm ci - run: npx playwright install --with-deps - run: npx playwright testExercise
- Read a real CI YAML in your repo. Identify trigger, jobs, steps, caches.
- Explain CI vs CD vs continuous deployment to a non-technical friend in 60 seconds.
- Sketch which stage each test type lives in (unit/integration/e2e).
Continuous Integration — Fowler
martinfowler.com
The original definitive essay.
GitHub Actions — Concepts
docs.github.com
Workflows, jobs, steps, runners.
Azure Pipelines — Key Concepts
learn.microsoft.com
Stages, jobs, agents — common in enterprise.
Working with Pipelines
Concept
Pipelines run on triggers: push, PR, manual dispatch, schedule. The UI streams logs per step. When something fails, scroll the failing step's logs — the cause is almost always in the last 50 lines. Differentiate pipeline failure (runner couldn't build — missing dep, syntax error) from test failure (build worked, assertion failed). Different fixes, different owners.
Example — manual trigger + input
on:
push: { branches: [main] }
workflow_dispatch:
inputs:
env: { type: choice, options: [dev, qa, prod] }
jobs:
test:
runs-on: ubuntu-latest
steps:
- run: npm test
env:
TARGET_ENV: ${{ github.event.inputs.env }}
Exercise
- Trigger a workflow manually with an input. Read the output.
- Find one failed run; classify: runner / build / test failure.
- Add a step that runs only on PR (
if: github.event_name == 'pull_request').
Managing Workflow Runs
docs.github.com
Re-run, cancel, debug, download artifacts.
GitLab CI/CD Pipelines
docs.gitlab.com
Concepts apply to most CI tools.
GitHub Actions in 60 minutes
YouTube / TechWorld with Nana
Hands-on intro, zero to deploying a real service.
Running Tests in CI
Concept
A test job needs three things: the right environment (Node/JDK version, browsers, DB), secrets as env vars (never in YAML), and a report uploaded as an artifact for triage. JUnit XML or HTML reports integrate with CI dashboards. Always fail the build on test failure (
Example
continue-on-error: false); flaky tests should be quarantined or fixed, never ignored.
jobs:
e2e:
runs-on: ubuntu-latest
env:
API_URL: ${{ secrets.QA_API_URL }}
AUTH_TOKEN: ${{ secrets.QA_TOKEN }}
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npx playwright test --reporter=junit,html
- if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: playwright-report/
Exercise
- Add a secret; reference it; print only its length to logs (never the value).
- Configure a JUnit-format report; upload as artifact; download and open it.
- Add a deliberately-failing test; verify the build goes red and the report shows the failure.
Using Secrets — GitHub Actions
docs.github.com
Storage, masking, scoping.
Playwright on CI
playwright.dev
Recipes for GitHub Actions, Azure, GitLab, Jenkins.
Test Reporter Action
github.com/marketplace
Render JUnit/Mocha output as PR check annotations.
07 · Mobile Automation
5 topics
Environment Setup — Appium, ADB, Emulators
Concept
Mobile automation has more moving parts than web. The stack: Appium server (a Node service that speaks WebDriver to mobile), device or emulator (Android Studio AVD / Xcode Simulator / real device with USB debugging), ADB for Android (install/uninstall apps, run shell), and your test code with the right driver. Desired capabilities tell Appium what device/app to drive: platformName, deviceName, app path, automationName.
Example — capabilities
// Android { platformName: 'Android', 'appium:deviceName': 'Pixel_7_API_34', 'appium:app': '/abs/path/app-debug.apk', 'appium:automationName': 'UiAutomator2', } # Useful ADB adb devices adb install app.apk adb shell pm list packages | grep myapp adb logcat | grep MyAppExercise
- Install Appium 2 + Android Studio. Boot an emulator. Run
adb devices. - Launch a sample app on the emulator via Appium with capabilities.
- Use
adb logcatto find the package name of an installed app.
Appium 2 Docs
appium.io
Install, drivers, capabilities, server config.
ADB Reference
developer.android.com
Every ADB command. Indispensable.
Android Virtual Devices
developer.android.com
Create and manage emulators.
Locators & Gestures
Concept
Appium Inspector is your DevTools for mobile — connect to a device and inspect the element tree. Prefer
Example — Appium tap & swipe
accessibility-id (cross-platform, set by developers) → resource-id (Android) / name (iOS) → XPath last resort. Gestures: tap, long-press, swipe, scroll, pinch, zoom. Modern Appium uses W3C Actions for gestures; older code uses TouchAction (deprecated).
// Find by accessibility-id await driver.$('~login-button').click(); // Swipe up (W3C actions) await driver.performActions([{ type: 'pointer', id: 'finger', parameters: { pointerType: 'touch' }, actions: [ { type: 'pointerMove', x: 500, y: 1500, duration: 0 }, { type: 'pointerDown', button: 0 }, { type: 'pointerMove', x: 500, y: 300, duration: 600 }, { type: 'pointerUp', button: 0 }, ], }]);Exercise
- Open Appium Inspector against a running app; identify 3 elements by accessibility-id.
- Automate a tap, a long-press, and a swipe.
- Negotiate stable accessibility-ids with your dev team for the 5 most-tested screens.
Appium Inspector
github.com/appium
The DevTools of mobile testing. Element tree + record/replay.
Appium Gestures
appium.io
W3C Actions API — tap, swipe, scroll, multi-touch.
iOS Accessibility (for accessibility-ids)
developer.apple.com
How iOS exposes IDs; what your dev team needs to set.
Mobile-Specific Scenarios
Concept
Mobile breaks in ways the web doesn't. Interrupts: incoming calls, SMS, push notifications, low-memory warnings — your test should survive (or assert correct recovery). Network conditions: cellular, weak Wi-Fi, offline transitions. Native vs webview contexts: hybrid apps embed a webview; you must
Example — context switch & network
switchContext() between NATIVE_APP and WEBVIEW_*. Permissions: location, camera, notifications — handled via Appium settings or pre-grant.
// Switch to webview inside a hybrid screen const ctxs = await driver.getContexts(); const webview = ctxs.find(c => c.startsWith('WEBVIEW')); await driver.switchContext(webview); // Simulate offline (Android) await driver.setNetworkConnection(0); // run a flow, assert offline UI shows await driver.setNetworkConnection(6); // wifi+dataExercise
- Toggle airplane mode mid-test; assert the offline state shows.
- Switch to webview in a hybrid screen; click an element inside it.
- Trigger an interrupt (incoming SMS via emulator command); verify the app recovers.
Appium — Hybrid Apps & Contexts
appium.io
Native ↔ webview switching, including ChromeDriver setup.
Emulator Console — Telephony
developer.android.com
Simulate calls, SMS, GPS from the command line.
BrowserStack App Automate
browserstack.com
Real-device cloud for cross-device runs without managing fleet.
Page Object Model for Mobile
Concept
Same idea as web POM, with two twists: screen objects (not pages) and platform-aware locators. Either keep one screen class with
Example
if (platform === 'iOS') branches (simple), or have LoginScreen.android.ts and LoginScreen.ios.ts behind a common interface (cleaner at scale). Lift gestures (scrollUntilVisible, swipeLeft) into a BaseScreen.
export class LoginScreen { constructor(private driver: WebdriverIO.Browser) {} user = () => this.driver.$('~login-username'); pass = () => this.driver.$('~login-password'); submit = () => this.driver.$('~login-submit'); async login(u: string, p: string) { await this.user().setValue(u); await this.pass().setValue(p); await this.submit().click(); } }Exercise
- Refactor a flat mobile test into a Screen Object.
- Add a platform branch for one element that differs between iOS and Android.
- Build a
scrollUntilVisible(label)helper in BaseScreen.
WebdriverIO — Page Objects
webdriver.io
Patterns for mobile screen objects with WebdriverIO + Appium.
Appium 1 → 2 Migration Guide
appium.io
Worth reading even if greenfield — explains current architecture.
Selenium PageObjects Wiki
github.com/SeleniumHQ
Cross-applicable principles. Older but still relevant.
Screenshot & Diagnostics on Failure
Concept
Mobile failures are harder to reproduce than web — different OS versions, permissions, network states. Capture as much diagnostic context as possible on every failure: screenshot, page source (XML element tree at moment of failure — gold for debugging), device logs (logcat / iOS syslog), video recording for the whole test run. Upload all of it as a CI artifact.
Example
afterEach(async ({ driver }, info) => { if (info.status === 'failed') { await driver.saveScreenshot(`fail-${info.title}.png`); const source = await driver.getPageSource(); fs.writeFileSync(`fail-${info.title}.xml`, source); // Android: pull logcat buffer execSync(`adb logcat -d > fail-${info.title}.log`); } });Exercise
- Add screenshot + page source + logcat capture on failure.
- Configure video recording for one suite run.
- Trigger a deliberate failure; verify all artifacts upload to CI.
Appium — Screenshots
appium.io
Screenshot APIs and OS quirks.
Appium Protocol Methods
github.com/appium
Full method reference, including page source and recording.
Logcat — Android Studio
developer.android.com
Filtering, log levels, capturing buffers.
08 · Design Patterns
4 topics
Page Object Model — Established Framework
Concept
Beyond "one class per page". A mature POM framework adds: a BasePage with shared waits/screenshots/navigation; page factory methods that return chained page objects (
Example — chained page objects
loginPage.login() → returns DashboardPage); component objects for repeated widgets (NavBar, Modal, DataTable); a fixture / DI layer that injects browser context. The result: tests read like business prose, page objects encapsulate the UI, and a redesign of one screen touches one file.
export class LoginPage extends BasePage { async login(u: string, p: string): Promise<DashboardPage> { await this.fill('#user', u); await this.fill('#pass', p); await this.click('button[type=submit]'); return new DashboardPage(this.page); } } // Test reads like prose test('admin sees user list', async ({ page }) => { const dashboard = await new LoginPage(page) .open() .then(p => p.login('admin', 'pw')); await expect(dashboard.userTable()).toBeVisible(); });Exercise
- Refactor 3 page objects to return the next page object on success actions.
- Extract a NavBar component object and reuse it across pages.
- Move shared waits to BasePage; delete duplicates.
Page Object — Martin Fowler
martinfowler.com
Original definition. Concise, definitive.
Playwright POM Pattern
playwright.dev
Modern POM in TypeScript with fixtures.
Selenium PageFactory
github.com/SeleniumHQ
Java
@FindBy annotation pattern. Ubiquitous in enterprise.Screenplay Pattern
Concept
Screenplay reframes tests as actors performing tasks against the system. Vocabulary: Actor (user persona with abilities), Ability (browse the web, query an API), Task (high-level intent:
Example — Serenity Screenplay
SignUp.with(email)), Action (low-level click/type), Question (assertion: TheText.of(...)). Trade-off: heavier ceremony than POM, but tests read beautifully — "Ada attempts to log in. She should see the dashboard." Best for large suites with shared business vocabulary.
actor.attemptsTo(
Open.url("/login"),
Enter.theValue("ada@x").into(LoginForm.EMAIL),
Enter.theValue("pw").into(LoginForm.PASSWORD),
Click.on(LoginForm.SUBMIT)
);
actor.should(seeThat(TheUrl.current(), containsString("/dashboard")));
Exercise
- Read one Screenplay test aloud — verify it reads as English prose.
- Convert one POM-based test into Screenplay; compare lines & readability.
- Write a custom
Taskfor "complete checkout".
Serenity Screenplay Fundamentals
serenity-bdd.github.io
The most-used Screenplay implementation. Java/TypeScript.
Serenity/JS
serenity-js.org
Screenplay for Node — works with Playwright, WebdriverIO, Cucumber.
Beyond Page Objects — InfoQ
infoq.com
Why Screenplay was invented. Honest about when to use it and when not to.
Singleton
Concept
A class that has exactly one instance, accessible globally. In test frameworks, common singletons: WebDriver/Browser instance, Config loader, Logger. Be careful — singletons make parallel tests painful (one shared driver across threads = race conditions). Modern frameworks prefer thread-local or DI/fixtures. If you do use singleton, document the threading contract.
Example — thread-safe driver holder
public class DriverFactory { private static final ThreadLocal<WebDriver> DRIVER = new ThreadLocal<>(); public static WebDriver get() { if (DRIVER.get() == null) DRIVER.set(new ChromeDriver()); return DRIVER.get(); } public static void quit() { if (DRIVER.get() != null) { DRIVER.get().quit(); DRIVER.remove(); } } }Exercise
- Implement a thread-local driver holder; run 4 tests in parallel; verify isolation.
- Build a Config singleton that loads
config.<env>.jsononce. - Identify one singleton in an existing project that should be a fixture instead.
Singleton — Refactoring Guru
refactoring.guru
Visual + multi-language examples. Honest about drawbacks.
Java ThreadLocal
docs.oracle.com
The right tool for per-thread driver instances.
Inversion of Control / DI — Fowler
martinfowler.com
Why DI usually beats singleton in modern test frameworks.
Page Factory
Concept
Page Factory is Selenium's annotation-based way to declare locators on page objects:
Example — Selenium Java
@FindBy(id = "submit") on a field, then PageFactory.initElements(driver, this) in the constructor — Selenium creates lazy proxies. Wins: less boilerplate, locator declarations live next to fields. Caveats: lazy proxies cache stale references on dynamic pages; not all bindings support it (Playwright's locators already lazy-evaluate, so PageFactory has no equivalent there). Useful in established Selenium-Java codebases.
public class LoginPage { @FindBy(id = "user") private WebElement user; @FindBy(id = "pass") private WebElement pass; @FindBy(css = "button[type=submit]") private WebElement submit; public LoginPage(WebDriver driver) { PageFactory.initElements(driver, this); } public void login(String u, String p) { user.sendKeys(u); pass.sendKeys(p); submit.click(); } }Exercise
- Convert a manual Selenium-Java POM to use Page Factory annotations.
- Trigger a stale element scenario; observe and fix.
- Compare line count vs the manual approach; decide which you prefer.
Selenium PageFactory Wiki
github.com/SeleniumHQ
Annotations, AjaxElementLocatorFactory, caveats.
Selenium — POM & Factory
selenium.dev
Official guidance, when to use which.
Page Factory in Selenium — ToolsQA
toolsqa.com
Hands-on tutorial with a complete project.
09 · Testing Approaches & Frameworks
5 topics
BDD — Behaviour-Driven Development
Concept
BDD writes tests as scenarios in business language (Gherkin: Given/When/Then) so non-engineers can read and even author them. The scenarios sit in
Example — Cucumber feature
.feature files; step definitions map each line to code. Win when you have product/business stakeholders who collaborate on tests; cost when developers do all the writing (the abstraction layer doesn't pay back). BDD is a collaboration practice, not a syntax — the value is in the conversation, not the Given/When/Then.
# features/checkout.feature Feature: Checkout Scenario: Successful purchase Given Ada is logged in When she adds "X1" to her cart And she completes checkout with a valid card Then she sees an order confirmationExercise
- Write 3 scenarios for a feature you own — read them with a non-engineer; iterate.
- Implement step definitions for one scenario.
- Decide for one team: is BDD paying for its overhead? Document the answer.
BDD — Cucumber Docs
cucumber.io
Honest framing: BDD is collaboration, Gherkin is just notation.
Introducing BDD — Dan North
dannorth.net
The original article. Read for the "why".
SpecFlow Learn
specflow.org
.NET-flavoured BDD. Same Gherkin, different runtime.
TDD — Test-Driven Development
Concept
Write a failing test first, write the smallest code to pass, refactor, repeat. The discipline forces you to think about behaviour and design before implementation, and gives you 100% test coverage as a side effect. Most useful for unit tests on logic-heavy code. Less useful for E2E (the iteration loop is too slow). For an SDET, even if your team isn't strictly TDD, writing the test before you fix a bug catches you reproducing it correctly.
Example — Red / Green / Refactor
// 1. RED — failing test test('discount of 10% on orders > $100', () => { expect(applyDiscount(120)).toBe(108); }); // 2. GREEN — minimal code to pass function applyDiscount(total: number) { return total > 100 ? total * 0.9 : total; } // 3. REFACTOR — extract a constant, add type, etc.Exercise
- Pick a small utility function. Write it TDD-style — test, code, refactor.
- Next bug you fix: write the failing test reproducing it before you fix.
- Try one Kata (e.g., Roman Numerals, FizzBuzz) entirely TDD.
TDD — Martin Fowler
martinfowler.com
The clearest one-page explanation.
Test-Driven Development by Example — Kent Beck
Book
The book that started it all. Short, practical.
Coding Dojo Katas
codingdojo.org
Curated practice problems perfect for TDD drills.
JVM Test Frameworks — TestNG & JUnit
Concept
JUnit 5 (Jupiter) is the modern standard for Java:
Example — JUnit 5 parametrised
@Test, @BeforeEach, @AfterEach, parameterised tests, parallel execution, extensions. TestNG predates modern JUnit and historically led on parallelism, data providers, and groups; today JUnit 5 has caught up on most fronts. Choose JUnit for new projects unless an existing TestNG-heavy codebase makes the switch costly. Both integrate with Maven/Gradle, Allure, IntelliJ, every CI.
@ParameterizedTest
@CsvSource({
"100, 100",
"120, 108",
"200, 180"
})
void applyDiscount(int input, int expected) {
assertEquals(expected, Cart.applyDiscount(input));
}
Exercise
- Set up JUnit 5 in a Maven project; run a parametrised test from
@CsvSource. - Configure parallel execution; verify thread isolation.
- Write a custom JUnit 5
Extensionthat takes a screenshot on failure.
JUnit 5 User Guide
junit.org
The complete reference. Skim once, refer forever.
TestNG Documentation
testng.org
Annotations, suites, data providers, groups.
JUnit 5 vs TestNG
baeldung.com
Honest side-by-side. Helps you choose.
Other Frameworks — NUnit, MSTest, Mocha, Jest, pytest
Concept
.NET: NUnit (most flexible, parametrised), MSTest (Microsoft's official, best Visual Studio integration), xUnit (more opinionated, popular in modern .NET). JS/TS: Mocha (minimal, BYO assertion lib), Jest (batteries-included, snapshot testing), Vitest (fast, ESM-native). Python: pytest (de facto standard, fixtures, parametrise, plugins). All share the same shape — describe/it (or @Test), setup/teardown, assertions — but the ecosystem and tooling differ. Match the framework to the language already on the project.
Example — pytest parametrise
@pytest.mark.parametrize("input,expected", [ (100, 100), (120, 108), (200, 180), ]) def test_apply_discount(input, expected): assert apply_discount(input) == expectedExercise
- Pick the framework matching your project's language. Set up a parametrised test.
- Configure parallel/sharded execution.
- Add one fixture (pytest) or hook (Mocha/Jest) that captures a screenshot on failure.
NUnit Documentation
docs.nunit.org
.NET unit testing. Attributes, constraints, parametrisation.
pytest Documentation
docs.pytest.org
The Python standard. Fixtures + parametrise are best-in-class.
Jest Getting Started
jestjs.io
Batteries-included JS test framework — runner + assertions + mocks + snapshots.
Cucumber / SpecFlow — BDD Frameworks
Concept
Cucumber runs Gherkin
Example — Cucumber-JS step
.feature files against step definitions in Java/JS/TS/Ruby/etc. SpecFlow is the .NET equivalent. Both integrate with any underlying test runner (JUnit, NUnit, Mocha) and any UI/API tool. The trap to avoid: writing tests in code first and then wrapping them in Gherkin. The order matters — write features first, with stakeholders, then implement the steps. Otherwise you've added a layer of indirection for no benefit.
import { Given, When, Then } from '@cucumber/cucumber'; Given('Ada is logged in', async function () { await this.loginPage.login('ada@x', 'pw'); }); When('she adds {string} to her cart', async function (sku) { await this.productPage.addToCart(sku); }); Then('she sees an order confirmation', async function () { await expect(this.page.locator('.confirmation')).toBeVisible(); });Exercise
- Set up Cucumber + Playwright (or SpecFlow + Selenium for .NET); run one scenario.
- Reuse a step definition across two scenarios; verify ambiguity warnings if regex collides.
- Generate an HTML/Allure report; share with a non-engineer for feedback.
Cucumber — Installation
cucumber.io
Setup for Java, JS/TS, Ruby. Linked from there to step writing.
SpecFlow Documentation
docs.specflow.org
.NET BDD. Note: maintenance status — check before greenfield.
Cucumber Anti-Patterns
automationpanda.com
Read before adopting BDD. Saves you from a year of regret.
10 · AI for Developers
6 topics
What is AI — ML, Deep Learning, LLMs
Concept
Machine Learning is the umbrella: algorithms that improve from data instead of being explicitly programmed. Deep Learning is a subfield using multi-layered neural networks. Large Language Models (LLMs) like Claude, GPT-4, and Gemini are deep learning models trained on enormous text corpora; they predict the next token, which turns out to be powerful enough to write code, summarise documents, and reason. For an SDET, the practical model: an LLM is a very fast junior dev who has read the internet — productive but not infallible. Your judgment matters more, not less.
Example — what an LLM actually does
// Prompt "Write a Playwright test for a login page with email, password, and a submit button. Assert the URL changes to /dashboard." // LLM output (sketch) test('login redirects to dashboard', async ({ page }) => { await page.goto('/login'); await page.fill('input[type=email]', 'a@x.com'); await page.fill('input[type=password]', 'pw'); await page.click('button[type=submit]'); await expect(page).toHaveURL(/\/dashboard/); }); // You verify selectors match the real page.Exercise
- Read the linked Anthropic intro. Define ML / DL / LLM in your own words.
- Ask an LLM to write a test for a real page you control. Run it. Note what it got wrong.
- Identify one task where AI saved you 20+ minutes this week.
Anthropic — Learn
anthropic.com/learn
Concise primers on how LLMs work and how to use them well.
AI for Everyone — Andrew Ng
Coursera — Free to audit
Cleanest framing of AI vs ML vs DL.
Intro to LLMs — Andrej Karpathy
YouTube
1-hour lecture. Best technical overview for engineers.
AI Coding Assistants — Copilot, Cursor, Claude Code
Concept
Three classes: inline completion (GitHub Copilot — ghost text as you type), chat-in-IDE (Copilot Chat, Cursor — multi-turn with file access), and agent CLIs (Claude Code, Aider — instruct in natural language, the agent runs commands and edits files). All pull from the same family of LLMs; what differs is the context window they can see and the actions they can take. Start with one inline tool plus one chat tool — enough to be productive without becoming dependent.
Example — same task, three tools
// Copilot (inline): you type a JSDoc, ghost-text completes /** Click each row's edit icon and assert it opens a modal */ test('edit row opens modal', async ({ page }) => { /* ... */ } // Cursor / Copilot Chat: select code, /test, "add a test" // Claude Code (CLI): "add a Playwright test that verifies // the edit button on /users opens a modal with role=dialog"Exercise
- Install Copilot in VS Code. Use it for an hour on real test code. Note one save and one mistake.
- Try the chat panel; give it a 50-line file and ask it to refactor.
- Compare the same prompt across two tools; observe which needed less correction.
GitHub Copilot Quickstart
docs.github.com
Inline completion + chat in 5 minutes.
Cursor IDE Docs
docs.cursor.com
Composer, agent mode, multi-file context.
Claude Code — Anthropic
docs.claude.com
Terminal agent that edits, runs, and tests. Strong for long sessions.
AI Safety — Reviewing AI-Generated Code
Concept
Treat AI output the way you'd treat a Stack Overflow answer: useful starting point, must be verified. Common failure modes: hallucinated APIs (calls a method that doesn't exist), outdated patterns (older library versions), silent insecurity (concatenated SQL, weak auth), license contamination (verbatim copies of GPL code). Always: read the diff, run the tests, lint, and run a security check. For sensitive logic (auth, money, PII), the bar is even higher — pair-program with the AI but ship only what you'd ship as your own.
Example — review checklist
BEFORE COMMITTING AI CODE [ ] Compiles / type-checks [ ] All asserted APIs actually exist (read the import) [ ] No string-concatenated SQL or HTML [ ] Secrets aren't hardcoded [ ] Same logging / error patterns as the rest of the codebase [ ] Tests pass — including a new one for the changed path [ ] License of any copy-pasted snippet is compatibleExercise
- Ask an AI for a "secure" auth helper. Audit it against the OWASP cheat sheet.
- Find one hallucinated API call in your AI history. Save the example.
- Run a license scanner (FOSSA free tier) on a small AI-assisted project.
OWASP Top 10 for LLM Apps
owasp.org
Prompt injection, insecure output handling, training-data poisoning.
Anthropic Safety Research
anthropic.com/research
Real-world failure modes of LLMs. Sharpens skepticism.
Responsible Use of Copilot
docs.github.com
Duplication detection, IP risks, review duties.
Advanced AI Tools — Copilot Advanced, Claude Code, Cursor
Concept
Beyond inline completion: Copilot's chat, edits, and workspace features let you ask "implement this PR" and see multi-file diffs. Cursor's Composer / agent mode chains tool calls (read file, run command, edit) for complex changes. Claude Code runs in your terminal, persists memory in
Example — multi-step session
CLAUDE.md, and is strongest on long-running multi-step refactors and codebase-wide questions. The skills that compound: writing self-contained prompts, attaching the right files, reviewing every diff, and committing in small steps.
# Claude Code — refactor a flaky suite
> read tests/login.spec.ts and tell me why this is flaky
> show me other suites in this repo that have the same issue
> refactor them all to use the auto-retrying matcher pattern
> run the suite and report failures
> commit with message: "fix: replace manual waits with auto-retry"
Exercise
- Use Cursor Composer or Claude Code for one multi-file refactor. Read every diff before accepting.
- Use Copilot's "Workspace" or chat with @workspace context to ask about an unfamiliar repo.
- Set up a
CLAUDE.md/.cursorruleswith your project's conventions; verify the agent respects them.
GitHub Copilot Chat
docs.github.com
@workspace, slash commands, multi-file edits.
Cursor — Composer / Agent
docs.cursor.com
Multi-file editing with tool use.
Claude Code Best Practices
docs.claude.com
How to structure long sessions, memory, and review loops.
AI for Test Automation — Generating Tests & Page Objects
Concept
AI is unusually good at the boring scaffolding of test automation: scaffolding new page objects from a URL, generating test data, writing JSON Schemas from sample responses, converting old waits to auto-retrying assertions. The pattern that works: let the AI draft, you decide what to keep. Treat the output as a first attempt and rewrite anything that doesn't match your conventions. Where AI is unreliable: anything requiring up-to-date selectors on your real app, anything depending on internal business rules it can't see, anything where "looks plausible" is more dangerous than "obviously wrong".
Example — page object from a URL
// Prompt "Open https://my-app.dev/checkout in your tools, snapshot the DOM, and generate a Playwright Page Object in TypeScript with: - private locators using getByRole/getByLabel - actions: fillCard, submit - returns ConfirmationPage on submit Match the style in pages/LoginPage.ts (attached)." // You then: review locators, run the test, fix anything stale.Exercise
- Generate a page object for one screen with AI. Audit every locator.
- Generate test data (50 plausible users) as JSON; spot-check for duplicates.
- Convert a 10-test suite from manual waits to auto-retrying matchers via AI; review every diff.
Playwright Codegen
playwright.dev
Record-and-replay generator. Pair with AI for cleanup.
Playwright MCP
github.com/microsoft
Model Context Protocol server — lets agents drive a browser to inspect and generate tests.
The 70% Problem — Addy Osmani
addyo.substack.com
Honest framing: AI gets you 70% there. The last 30% is the engineer.
AI-Assisted Debugging — Errors & Logs
Concept
Debugging is one of AI's strongest use cases when you give it the right inputs. Paste the error message verbatim (don't summarise), the relevant code, the stack trace, and what you've already ruled out. For log triage, paste the failing log lines plus surrounding context (10 lines before/after). Ask the model to enumerate hypotheses ranked by likelihood, then verify each yourself — don't accept the first explanation. The model's job is to widen your search; yours is to narrow it.
Example — debugging prompt
"Playwright test fails on CI but passes locally:
test('cart updates', async ({ page }) => {
await page.click('.add-to-cart');
expect(await page.locator('.cart-count').textContent()).toBe('1');
});
CI error: 'Expected 1, received 0' on line 4.
Locally: passes 10/10 runs.
What I've ruled out: the selector matches (verified in DevTools).
Give me 5 hypotheses ranked by likelihood, with the smallest
investigation step for each."
Exercise
- Take a recent flaky failure. Paste error + code + ruled-out into AI; rank the hypotheses against your own.
- Paste a 100-line failing log; ask AI to identify the first interesting line.
- For one bug, document the 3 wrong hypotheses AI suggested before the right one — sharpens prompting.
Be Clear and Direct — Anthropic
docs.claude.com
Highest-leverage prompting habit, with paired bad/good examples.
Exploring Gen AI — Fowler & team
martinfowler.com
Field notes from senior engineers using AI on real projects. Honest.
Simon Willison's LLM Notes
simonwillison.net
Daily, practical writing about real LLM use. Best feed in this space.
11 · Prompt Engineering
6 topics
Writing Prompts for Code
Concept
A vague prompt gets vague output. A good code prompt names: language & framework ("TypeScript, Playwright"), the task ("write a test that..."), inputs & assertions ("URL changes to /dashboard, success toast appears"), and constraints ("no sleeps, use auto-waits, follow existing POM"). Think of it like writing a ticket for a junior dev — they can't read your mind, but they're fast and tireless.
Example — vague vs specific
// VAGUE "write a login test" // SPECIFIC "Write a Playwright test in TypeScript for our LoginPage POM (pages/LoginPage.ts). It should: - visit /login - call loginPage.login(adaUser.email, adaUser.password) - assert URL matches /dashboard - assert .toast--success is visible Use only existing helpers; don't add new dependencies."Exercise
- Take a vague prompt you wrote and rewrite it with the 4 elements above.
- Run both versions; diff the outputs.
- Save the best 3 prompts you write this week as templates.
Anthropic Prompt Engineering Guide
docs.claude.com
Best concise, model-agnostic guide. Read top-to-bottom once.
Prompt Engineering Guide
promptingguide.ai
Open-source comprehensive resource — techniques, examples, papers.
Better Prompts for Copilot
github.blog
Code-specific prompting habits — comments as prompts, examples as anchors.
Basic Patterns — Write / Explain / Fix
Concept
Three patterns get you 80% of the way: "Write X" (generation — provide signature, examples, constraints), "Explain X" (comprehension — paste code, ask for plain-language summary), "Fix X" (debugging — paste failing code and the error message). Always include the error verbatim — the model can't guess what went wrong from "doesn't work".
Example — Fix this code
"This Playwright test is flaky on CI but passes locally:
test('cart updates', async ({ page }) => {
await page.click('.add-to-cart');
expect(await page.locator('.cart-count').textContent()).toBe('1');
});
Error on CI: 'Expected 1, received 0'.
Why is it flaky? Show the fix using auto-waiting assertions."
Exercise
- Use each pattern once today on real test code.
- Take a recent bug; ask AI to explain the root cause from the diff. Compare to your own analysis.
- Build 1-line shortcut prompts:
/refactor,/explain,/test-this.
Be Clear and Direct — Anthropic
docs.claude.com
The single highest-leverage prompting habit.
OpenAI Prompt Engineering Guide
platform.openai.com
Six strategies including "give models time to think".
Awesome ChatGPT Prompts
github.com/f
Massive prompt library. Filter for "code reviewer", "test writer".
Learning from AI Suggestions
Concept
When the AI proposes something you didn't know — a new selector strategy, a library, a regex, a SQL function — pause and learn it before accepting. Otherwise you'll accumulate code you can't maintain. Habits that compound: read every line before accepting, ask "why this approach?" in chat, open the docs for any unfamiliar API, simplify if you can't justify a piece. AI makes you better, not lazier — if you let it.
Example — interrogate the suggestion
// AI suggested: await page.locator('role=button[name="Save"]').click(); // You ask: "Why role= over a CSS selector? When would CSS be better?" // Now you actually understand role-based locators — // the next time you'll choose, not copy.Exercise
- Pick one AI suggestion you accepted last week. Find the docs for every API it used.
- Replace one AI suggestion with a simpler one you wrote yourself. Compare readability.
- Keep a notebook: 1 line per useful pattern AI taught you each week.
The 70% Problem — Addy Osmani
addyo.substack.com
Why AI gets you 70% there but the last 30% separates good engineers.
Exploring Gen AI — Fowler
martinfowler.com
Field notes from senior engineers using AI on real projects.
Simon Willison's LLM Notes
simonwillison.net
Daily, practical LLM writing.
Code Generation Prompts — Framework-Specific
Concept
Generic prompts get generic code. The leverage move: encode your framework choices into the prompt itself. Pin the language, the test framework, the assertion style, and the existing POM file by reference. For refactoring, paste the before file and describe the after — let the model produce the diff, not the rationale.
Example — refactor prompt
"Refactor this test to:
- Use Playwright auto-retrying matchers (no `await ... .textContent()`)
- Replace `page.locator('css...')` with `getByRole` / `getByLabel`
where roles are clear
- Keep the assertion semantics identical
Match the style in pages/LoginPage.ts (attached).
[paste current test]
Output: only the modified file, no commentary."
Exercise
- Build a "framework prompt" template for your stack (1 paragraph) — reuse it everywhere.
- Refactor 3 tests with the template; compare diffs to your manual fixes.
- Generate a new test from a 1-sentence description that includes the framework prompt.
Prompting Techniques
promptingguide.ai
Catalogue with code examples for each.
Use Examples (Multishot) — Anthropic
docs.claude.com
Why showing 2-3 examples beats describing the rule.
Prompting Patterns — Eugene Yan
eugeneyan.com
A senior engineer's tested patterns. Strong code-focused signal.
Prompt Techniques — Few-Shot & Chain of Thought
Concept
Few-shot prompting: include 2-3 input/output examples; the model imitates the pattern. Beats describing the rule for anything tricky (custom selectors, naming conventions, log parsing). Chain of thought (CoT): ask the model to "think step by step" before answering — works well for debugging, math, and decision questions where reasoning matters more than fluency. Modern models often do CoT internally; you still benefit from explicit "list 3 hypotheses, then pick" prompts.
Example — few-shot
"Convert each test name to our convention.
Examples:
test('it should login') → test('logs in with valid credentials')
test('check 404 on bad id') → test('returns 404 for unknown order id')
test('login broken') → test('shows error for invalid password')
Now convert:
test('add to cart works')
test('admin only')
test('save fails when offline')"
Exercise
- Pick a tricky transformation in your codebase. Write a 3-shot prompt; verify accuracy.
- Use "list 3 hypotheses, then recommend one" on a real bug.
- Compare zero-shot vs few-shot output for the same task; note quality difference.
Chain-of-Thought — Anthropic
docs.claude.com
When and how to invoke explicit reasoning.
Chain-of-Thought Paper (Wei et al.)
arxiv.org
The original. Surprisingly readable.
Few-Shot Prompting
promptingguide.ai
Worked examples and pitfalls.
SDET-Specific Patterns — Framework / API / CI Prompts
Concept
Prompts you'll reuse weekly:
Example — CI/CD prompt
- Framework setup: "Scaffold a new Playwright project with TS, ESLint, GitHub Actions, JUnit reporter, and a sample POM."
- API automation: "Given this OpenAPI spec, generate Playwright API tests covering happy path + 4xx for each endpoint."
- UI automation: "Given this URL and the LoginPage style, generate a Page Object + 3 E2E tests."
- CI/CD config: "Write a GitHub Actions workflow that installs deps, runs unit + E2E, uploads the report on failure, and deploys to staging on main."
- Test data: "Generate 20 plausible users as JSON with realistic email/role distribution."
"Write a GitHub Actions workflow .github/workflows/e2e.yml:
- Triggers: push to main, PR
- Node 20, Ubuntu latest
- Install deps with `npm ci`
- Install Playwright browsers
- Run `npx playwright test --reporter=junit,html`
- On failure, upload `playwright-report/` as an artifact
- Pass secrets QA_API_URL, QA_TOKEN as env vars
Output the YAML only, no commentary."
Exercise
- Build a 5-prompt library covering: framework setup, API tests from OpenAPI, POM scaffolding, CI YAML, test data.
- Run each on a fresh dummy project; refine until output requires no edits.
- Share your library with one teammate; iterate based on feedback.
Playwright MCP
github.com/microsoft
Lets agents drive a browser to write tests against your real app.
OpenAPI Specification
swagger.io
Feed it to AI; get full API test scaffolding.
Anthropic Cookbook
github.com/anthropics
Real-world prompt examples by domain. Adapt for SDET.
12 · Context Engineering
5 topics
Code Context Fundamentals — What the AI Needs
Concept
An LLM only knows what's in the prompt. Context engineering is the discipline of choosing what to include so the model can do its job without guessing. The minimum useful context: file structure (where files live, what to import), relevant existing code (so it matches your style), and dependency info (versions and what's available). Too little → hallucinated APIs. Too much → noise drowns signal and you blow the context window. Aim for the smallest set that lets the model see the patterns it must follow.
Example — bare vs context-rich
// BARE "Write a Playwright test for the cart" // CONTEXT-RICH "Project structure: pages/CartPage.ts (POM with addItem, getCount, checkout) fixtures/users.json (uses ada@x.com, role: admin) playwright.config.ts (baseURL, no implicit waits) package.json (Playwright 1.45, TS 5.4) Existing CartPage methods: await cart.addItem(sku); expect(await cart.getCount()).toBe(N); Write a test that adds 3 distinct SKUs and asserts count is 3."Exercise
- Write a prompt for a real change with no context. Save the output.
- Write the same prompt with 3 files of context. Diff the quality.
- Identify the minimum file set the AI needed (often 2-3 files).
Contextual Retrieval — Anthropic
anthropic.com/news
How to add retrieval to give an LLM the right snippets without bloat.
Context Engineering — Phil Schmid
philschmid.de
Practical primer on the term and the discipline.
Cursor — Context Overview
docs.cursor.com
@-mentions, codebase indexing, rules. Concrete patterns.
Providing Code Context — Files, Architecture, Rules
Concept
Three high-leverage techniques: (1) Reference files explicitly — paste them, attach them, or use
Example — project rules file
@filename in tools that support it. (2) Describe architecture briefly — "POM-based, fixtures in JSON, no implicit waits" beats making the AI guess. (3) Pin a project rules file — a one-pager (CLAUDE.md, .cursorrules, .github/copilot-instructions.md) that every prompt sees automatically. The rules file is the single biggest force multiplier: write it once, every future prompt benefits.
# CLAUDE.md / .cursorrules ## Stack - Playwright 1.45 + TypeScript, POM in `pages/` - Tests in `tests/`, fixtures in `fixtures/` - No `Thread.sleep` — use `expect(...).toBeVisible()` style ## Conventions - File names: kebab-case - Locators: prefer `getByRole`, fall back to CSS, avoid XPath - One assertion per behaviour; meaningful messages ## Don't - Don't add new dependencies without asking - Don't write comments that just restate codeExercise
- Write a 1-page rules file for your project. Test it with 3 prompts.
- Reference 2 files in a prompt explicitly; compare to a no-context run.
- Add a 3-bullet "architecture" header to your standard prompt template.
Claude Code — CLAUDE.md Memory
docs.claude.com
Project memory files the agent loads automatically.
Cursor — Project Rules
docs.cursor.com
.cursorrules + .cursor/rules/ — same idea for Cursor.Copilot Custom Instructions
docs.github.com
.github/copilot-instructions.md — GitHub-native pinned context.Repository-Aware Prompting — Project Structure & Patterns
Concept
Repo-aware tools (Cursor's codebase index, Copilot's
Example — onboarding for an agent
@workspace, Claude Code's read-tool) let agents discover context themselves. Your job: structure the repo so it can. Predictable layout (consistent folders, kebab-case files) → easier for both humans and agents. An onboarding doc (README.md + ARCHITECTURE.md) → the agent can read it and explain the project back to you. Index-friendly conventions: descriptive filenames, module-level JSDoc, consistent imports.
# ARCHITECTURE.md This is an SDET test repo for the Acme product. - `tests/api/` — Playwright API tests (smoke + regression) - `tests/e2e/` — Browser E2E using POM in `pages/` - `pages/` — One file per page; extends BasePage - `fixtures/` — Per-test setup (uses Playwright fixtures pattern) - `clients/` — Typed API clients used by both API tests and E2E setup Run: `npm test` (everything) `npm run smoke` (PR gate) `npm run e2e` (nightly)Exercise
- Write a 1-page
ARCHITECTURE.mdfor your repo. Test by asking an AI to describe the project back. - Find one inconsistent folder/naming pattern; fix it.
- Use
@workspaceor codebase indexing on a real change; observe what context the agent picked.
Cursor Codebase Indexing
docs.cursor.com
How the index is built and used; tips to make it more accurate.
Copilot @workspace
docs.github.com
Repo-aware Q&A in VS Code/JetBrains.
ARCHITECTURE.md — matklad
matklad.github.io
Why every repo benefits from one. Short and persuasive.
Multi-File Context — Related Files & Test/Code Pairs
Concept
Most non-trivial changes touch multiple files; a prompt with only one file produces a change that ignores the others. The pattern: name the related files explicitly in the prompt, then either paste them or use
Example
@ references. For tests, the most important pair is test + page object + fixture — all three need to be visible for the AI to match your style. For code changes, include the caller sites so the AI can update them in the same response.
"Update CartPage.checkout() to take an optional couponCode.
Files involved:
- pages/CartPage.ts (the method)
- tests/checkout.spec.ts (existing test — update to pass undefined)
- tests/coupon.spec.ts (NEW — verify discount applies)
Update all three in one diff. Match existing styles in each file."
Exercise
- Pick a small change touching 3 files. Write one prompt that updates all three; verify the diffs.
- Find one PR you wrote where the AI missed a caller — write the prompt that would have caught it.
- Build a "test/page/fixture trio" prompt template and reuse for next 3 features.
Cursor @-Symbols
docs.cursor.com
@file, @folder, @code, @docs — multi-file context made explicit.
Claude Code — File References
docs.claude.com
How to attach and reference files in CLI sessions.
Quantified Copilot Impact — GitHub
github.blog
Where multi-file context helps most, with empirical numbers.
Documentation as Context — Specs & Requirements
Concept
For test generation, the highest-quality context isn't always code — it's the spec. An OpenAPI/Swagger doc tells the AI every endpoint, method, parameter, status code, and schema. A Cucumber
Example — generate API tests from OpenAPI
.feature file lists the exact scenarios. A Postman collection gives concrete examples. When the spec is up-to-date, generated tests are dramatically better. When it isn't, that's a separate problem to fix — and the AI exposing the drift is doing you a favour.
"Attached: openapi.yaml (full spec for our orders API).
Generate Playwright API tests in tests/api/orders/ covering:
- Happy path for each endpoint
- 400 / 401 / 403 / 404 / 422 cases per the spec's responses
- One schema-validation test per endpoint using zod
Use our base client `clients/orders.ts` (also attached).
Output: one .spec.ts file per endpoint, no commentary."
Exercise
- Generate API tests from your team's OpenAPI spec; review what the AI got right and wrong.
- Where the spec was wrong, file a docs ticket — that's value the AI surfaced.
- Pair a feature file with a prompt to generate step definitions; review for accuracy.
OpenAPI Specification
swagger.io
The contract format your backend probably already publishes.
Postman API Documentation
postman.com
Auto-generated from collections — feed to AI for test scaffolding.
Long Context Tips — Anthropic
docs.claude.com
How to attach big specs without losing the model's attention.