Background

Over the last week, I’ve been implementing a risk-based framework to assist with testing software products and documenting evidence for audits.

The products I work on typically deal with PII, worker and employer data, payrolling, staffing and placement, benefits, prescreening, compliance, Vendor Management Systems (VMSs), Managed Service Providers (MSPs), and all kinds of other fun stuff.

Additionally, the products serve clients domestically in the US and globally in jurisdictions across the EU, the UK, and Canada.

The Problem

The friction I was initially facing was how I could realistically keep testing and QA on pace with the speed of the developers on my team in the age of agentic coding.

Until recently, when Claude Code was rolled out to my company’s QA team, testing our products was done mostly manually.

Regression testing was painful and slow, taking days on end to complete with a team of 4 QA engineers.

Automation was essentially non-existent, and what did exist were a handful of unit and UI tests that weren’t running in CI/CD, so they weren’t providing any real value in catching regressions or ensuring quality.

The Solution

Manual testing was immediately deprioritized, but not completely eliminated, and risk-based automation testing was immediately prioritized.

Leveraging testing frameworks and best practices such as The Test Pyramid, functional testing, and non-functional testing, I developed a single workflow with five phases for testing software products:

  • Classify: High-Risk or Standard. HR products are High-Risk by default.
  • Measure: 20 test categories from the tool output.
  • Score: Invest / Keep / Hold / Drop
  • Gate: Test suites earn merge-blocking status after an advisory period. The smoke test suite blocks from day 1.
  • Govern: Dual sign-off, escalation paths, and quarterly reviews.

The Defaults

Every project includes the following default test suites:

  • Unit
  • Security - Role-based access control (RBAC)
  • UI - Smoke & E2E
  • Regression

The Definitions

CategoryDefinitionCoverage signal to report
UnitSmallest testable units in isolationLine/branch % (v8 or equivalent)
IntegrationComponents working together.Name the layer(s): database (RLS/pgTAP), service (internal API), cross-system (payroll/identity connectors); they are different tests Multi-jurisdiction / EOR connectors are a distinct sub-layer; decompose and report per jurisdiction in the audit fill% of entities or integration points covered, per layer
Integration — Third-Party Data HandlingCompliance posture of data exchanged with external vendors (payroll processors, background-check vendors, E-Verify, tax-filing APIs): encryption in transit, audit trail of what was sent/received, retention alignment, SLA verification; distinct from whether the connector works.Drop-by-default for early products, but the row must exist so the question has a home when a vendor integration ships
System / E2EFull user journeys through the real stack (Playwright)% of high-risk modules with ≥1 spec
SmokeCritical-path checks pre-release.Absorbs Sanity by default. Split only if the deploy pipeline diverges from the release pipeline
API / InterfaceMerged category: route-level functional behavior and contract/schema validation.% of routes tested; schema validation present Y/N
RegressionExisting suites re-run on a cadence; usually satisfied via unit/E2E gatesCadence + gate reference
MutationDo the existing tests actually assert anything (Stryker)?Mutation score (% mutants killed)
Security — Access Control & Data BoundariesAuthZ/RLS correctness plus data-boundary assertions: right data visible to right roles across PII / pay / identity tiers; no cross-tenant or cross-client bleed.For SOC 2-scoped products, note which controls (e.g., CC6.1; Logical Access & Information Protection) these tests evidence
Security — Vulnerability ScanningSAST, dependency scanning, secrets detection. Three distinct failure modes; report eachPer sub-scope: SAST Y/N, deps Y/N, secrets Y/N
PerformanceResponse-time measurement against a stated budgetStated budget + measured p95
LoadBehavior at expected concurrent volumePass/fail at stated volume
StressBehavior beyond expected volume; failure modes and recoveryPass/fail at stated ceiling
ReliabilitySustained-operation correctness: concurrency, retries, idempotency, crash recoveryCount of verified failure-mode scenarios
CompatibilityBrowser/device/version matrix actually exercised# of targets in CI vs. supported list
AccessibilityWCAG conformance at component level (vitest-axe) and flow level (E2E + axe)% of components scanned; # of flows scanned
UsabilityFormal user studies or heuristic reviewsSessions run
Globalizationi18n readiness: framework, encoding, RTL, date/number formatsN/A until an i18n framework exists
LocalizationTranslated-content correctness per localeLocales verified
ExploratoryStructured session-based exploration beyond scripted testsSessions logged

The Framework

CategoryTooling & pipeline refCoverage signalCI gatePriorityOwnerRevisit Trigger
Unit
Integration
Integration — Third-Party Data Handling
System / E2E
Smoke
API / Interface
Regression
Mutation
Security — Access Control & Data Boundaries
Security — Vulnerability Scanning
Performance
Load
Stress
Reliability
Compatibility
Accessibility
Usability
Globalization
Localization
Exploratory

The Example

CategoryTooling & pipeline refCoverage signalCI gatePriorityOwnerRevisit trigger (Hold & Drop)
UnitVitest 4 + v8>= 80% line/branch coverageRequired-PR (ci.yml) — coverage threshold gate enabledKeepQA Lead
IntegrationRLS (test/rls) + pgTAP — database layer88% of entities (15/17)Advisory-PR (ci.yml) — intended Required, branch protection pendingHoldQA LeadPending flip from Advisory-PR to Required-PR
System / E2EPlaywright, 23 specs83% of HIGH-risk modules (5/6)Nightly critical subset (nightly-e2e.yml); full run Weekly (weekly-e2e.yml)InvestQA Lead
SmokePlaywright smoke config (playwright.smoke.config.ts)100% of critical pathsAdvisory-PR — intended Required from day oneKeepQA LeadSanity merged: no separate deploy pipeline, so post-deploy sanity = smoke. Split only if deploy pipeline diverges
API / InterfacePartial route tests (Vitest) + 2nd Playwright project overlappingPartial route coverage; schema/contract validation 0%NoneHold — consolidate the two overlapping suites before growingQA LeadContract tier: first external API consumer
RegressionUnit + E2E suites on cadenceCurrently satisfied via unit/E2E gates (no separate suite needed while those run on cadence)Advisory-PR + Nightly (via unit/E2E gates; branch protection pending)KeepQA Lead
MutationStryker (stryker.config.json, stryker.integration.config.json)Break threshold enforced at 60 (high:80 / low:60 / break:60, stryker.config.json), unit scope; latest run’s score not recorded, pull from mutation.yml artifactWeekly (mutation.yml — cron Sun 06:00 UTC + manual dispatch); break:60 enforced on unit scope. Integration scope (stryker.integration.config.json) wired but intentionally not in CI until PR3/PR4 integration tests existHold — record the latest score, then re-score (Keep or Invest)QA LeadPromote once the latest mutation score is recorded in the audit
Security — Access Control & Data BoundariesRLS (test/rls) + pgTAP; evidences SOC 2 CC6.1 (Logical Access & Information Protection)88% of entities with RLS tests (same RLS suite as the Integration row — two lenses, not double-counted); cross-tenant / pay-tier boundary assertions not yet namedAdvisory-PR (ci.yml) — intended Required, branch protection pendingHoldQA LeadPromote to Invest when cross-tenant and pay-tier boundary assertions are named, written, and CI-gated. SOC 2: these tests evidence CC6.1 only — map the rest of the CC6/CC7/C1 cluster (RBAC, audit logging, encryption, access review, classification) to test rows or named non-test controls before any SOC 2 audit (owner: QA Lead)
Security — Vulnerability ScanningDependabot (deps). Secrets: GitHub native secret scanning — enable this sprint (free on Team tier, one config change). SAST: noneDeps: Y · Secrets: N (enabling — Invest) · SAST: NNone (Dependabot runs async; secret scanning enforces platform-side once enabled)Invest (secrets) / Hold (SAST)QA LeadSAST: adoption decision (Semgrep or CodeQL), owner Infra team — wire as Advisory-PR, escalate per the Appendix A gate lifecycle. Secrets: no trigger enable now
PerformanceNone0% — no toolingNoneDropQA LeadReal traffic scale number
LoadNone0% — no toolingNoneDropQA LeadReal traffic scale number
StressNone0% — no toolingNoneDropQA LeadReal traffic scale number
Reliability1 targeted concurrency test1 verified failure-mode scenarioAdvisory-PR (ci.yml unit job — branch protection pending)HoldQA LeadExpand scenarios at real scale or first concurrency incident
CompatibilityPlaywright — chromium only1 of 1 CI-exercised browser (chromium); no other targets declaredAdvisory-PR (smoke; branch protection pending) + Nightly (E2E), chromium onlyHoldQA LeadMulti-browser demand signal (stakeholder request or non-chromium usage in analytics)
Accessibilityvitest-axe (component tier); 0 E2E flows~71 components scanned; 0 flows scannedAdvisory-PR (component tier — branch protection pending) / None (E2E tier)Invest — E2E tier (org-wide internal tool: near-external weight, §2)QA Lead
UsabilityNone0 sessionsNoneDropQA LeadDedicated QA/UX hire
GlobalizationNone — no i18n frameworkN/ANoneDropQA LeadMulti-country UI rollout
LocalizationNoneN/ANoneDropQA LeadMulti-country UI rollout
ExploratoryInformal QA triage (session notes, no formal charters)0 formal sessions loggedNoneDropQA LeadDedicated QA hire; keep lightweight session notes meanwhile