QA Standards
A living reference of the standards I hold my own testing work to: the tech stack I work in, best-practice code, test case templates, commenting conventions, and bug report templates.
Tech Stack
The tools I reach for and why, not just a list of logos. What's actually behind each choice comes down to the project, the team, and what's already in place.
Automation Frameworks
Default for new suites — built-in auto-waiting, multi-browser/multi-context support, and parallelization that scales without extra tooling.
Fast, tightly-scoped JS/TS suites, especially on smaller SPAs where its in-browser execution model and DX are a strong fit.
Still the right call on legacy stacks or when a team's existing grid infrastructure is already built around it.
Mobile automation across iOS and Android, sharing the WebDriver protocol so patterns carry over from Selenium suites.
Keyword-driven suites for teams that need non-engineers reading and contributing to test cases.
Languages
Default for new Playwright/Cypress suites — types catch selector and fixture mistakes before a test ever runs.
Cypress suites and any Node-based tooling wrapped around a pipeline.
Data-heavy test scripting, quick API checks, and Robot Framework libraries.
NUnit/Playwright suites on .NET-shop teams, plus reusable page-object and utility libraries.
Verifying backend state directly — the fastest way to confirm a UI action actually persisted correctly.
CI/CD & API
Pipeline of choice for repos already living on GitHub — matrix runs across browsers with minimal YAML.
CI/CD on Azure DevOps-hosted projects, wired into the same boards the rest of the team already tracks work in.
Legacy pipelines with heavy customization needs that a hosted CI can't easily replicate.
API contract checks and quick manual verification before automating a new endpoint.
Load and performance testing scripted in JS, integrated into the same pipeline as the functional suites.
Test Case Management
Project & Reporting Tools
AI Software Testing Tools
Databases & Monitoring
UX/UI Flow & Design
Browsers
Operating Systems
Other Tools & Platforms
Testing Disciplines
Best Practice Code
A few principles I hold every automation suite to, with a quick before/after so the reasoning is concrete rather than abstract.
Locator Strategy
Structural selectors break the moment a class name or DOM depth changes. Role and test-id locators describe intent instead of markup, so they survive redesigns.
// Breaks the moment a class name or DOM depth changes
await page.locator('div.container > ul > li:nth-child(3) > button.btn-primary').click();
// Survives most markup and styling changes, and reads like a spec
await page.getByTestId('checkout-submit').click();
// When there's no test-id, an accessible role query works just as well
await page.getByRole('button', { name: 'Submit order' }).click();
No Hard Sleeps
A fixed waitForTimeout is either too short under load or wastes time
on every run. Assertions that auto-retry wait exactly as long as needed and no
longer.
await page.click('#save-button');
await page.waitForTimeout(3000); // guessing how long the save takes
expect(await page.locator('.toast-success').isVisible()).toBe(true);
await page.click('#save-button');
await expect(page.locator('.toast-success')).toBeVisible(); // retries until it appears or times out
Reusable Soft Assertions
A hard assertion stops a test at the first failure, hiding every other broken expectation behind it. Collecting failures and verifying once at the end surfaces everything wrong with a page in a single run.
public sealed class SoftAssertions
{
private readonly List<Exception> _failures = new();
public void Check(Action assertion, string label)
{
try
{
assertion();
}
catch (Exception ex)
{
_failures.Add(new Exception($"{label}: {ex.Message}"));
}
}
public void Verify()
{
if (_failures.Count == 0) return;
var messages = string.Join("\n", _failures.Select(f => f.Message));
throw new AssertionException($"{_failures.Count} soft assertion(s) failed:\n{messages}");
}
}
Test Case Templates
Copy/paste starting points for new tests, structured so the intent of each test is readable before you even look at the assertions.
Playwright + NUnit — Given/When/Then Smoke Test
Category tags keep the smoke suite runnable independently from full regression.
[Test]
[Category("smoke")]
[Category("regression")]
public async Task Test_Login_ValidCredentials_RedirectsToDashboard()
{
// Given I am on the login page
await Page.GotoAsync(Config.BaseUrl + "/login");
// When I submit valid credentials
await Page.FillAsync("#email", Config.TestUser.Email);
await Page.FillAsync("#password", Config.TestUser.Password);
await Page.ClickAsync("#login-submit");
// Then I land on the dashboard
await Expect(Page).ToHaveURLAsync(new Regex(".*/dashboard"));
await Expect(Page.Locator("h1")).ToContainTextAsync("Welcome back");
}
Cypress — Test Structure & Custom Commands
Shared setup lives in a custom command, not repeated in every test, so each test body is just the behavior actually under test.
describe('Checkout flow', () => {
beforeEach(() => {
cy.login(Cypress.env('testUser')); // custom command, keeps setup out of each test
cy.visit('/cart');
});
it('lets a signed-in user complete checkout with a saved card', () => {
cy.get('[data-cy=checkout-button]').click();
cy.get('[data-cy=saved-card-0]').click();
cy.get('[data-cy=place-order]').click();
cy.get('[data-cy=order-confirmation]').should('be.visible');
cy.get('[data-cy=order-number]').invoke('text').should('match', /^ORD-\d+$/);
});
});
Bug Report Template
A structure that gives a developer everything needed to reproduce a defect without a follow-up thread, whatever tracker it ends up in.
## Summary
One sentence describing the defect.
## Environment
- Browser/App version:
- OS:
- Environment: (local / staging / production)
## Preconditions
What state the system needs to be in before reproducing.
## Steps to Reproduce
1.
2.
3.
## Expected Result
## Actual Result
## Severity / Priority
Severity: (blocker / critical / major / minor / trivial)
Priority: (P1 / P2 / P3)
## Evidence
Screenshots, screen recording, console logs, network traces.
## Additional Notes
Anything that doesn't fit above — workarounds, related tickets, frequency.
Commenting Templates
Comments that explain intent and impact age far better than comments that narrate what the code already says.
What It Tests / Why It Matters
A two-line header above every test so a failure report is legible to someone who has never opened the test file.
C# XML Doc Comments
Every public helper gets a summary, so IntelliSense carries the "why," not just the signature.
JSDoc
Same idea in JavaScript — types and defaults documented at the call site, not just in the implementation.