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.

C#

NUnit/Playwright suites on .NET-shop teams, plus reusable page-object and utility libraries.

SQL

Verifying backend state directly — the fastest way to confirm a UI action actually persisted correctly.

  • Java

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.

k6

Load and performance testing scripted in JS, integrated into the same pipeline as the functional suites.

Test Case Management

AI Software Testing Tools

UX/UI Flow & Design

Operating Systems

Other Tools & Platforms

Testing Disciplines

  • Functional
  • Regression
  • API
  • Cross-Browser
  • Accessibility (a11y)
  • Performance
  • Mobile
  • Visual Regression
  • Exploratory

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.

Avoid · Playwright TypeScript
// Breaks the moment a class name or DOM depth changes
await page.locator('div.container > ul > li:nth-child(3) > button.btn-primary').click();
Prefer · Playwright TypeScript
// 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.

Avoid · Playwright TypeScript
await page.click('#save-button');
await page.waitForTimeout(3000); // guessing how long the save takes
expect(await page.locator('.toast-success').isVisible()).toBe(true);
Prefer · Playwright TypeScript
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.

C# · NUnit
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.

C# · NUnit
[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.

JavaScript · Cypress
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+$/);
  });
});

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# · NUnit
// What It Tests: The checkout page shows an inline error when a declined card is submitted.
// Why It Matters: A silent failure here leaves the customer stuck with no path to complete an order.
[Test]
public async Task Test_Checkout_DeclinedCard_ShowsInlineError() { /* ... */ }

C# XML Doc Comments

Every public helper gets a summary, so IntelliSense carries the "why," not just the signature.

C#
/// <summary>
/// Waits for the given locator to become visible, then clicks it. Retries the wait once if
/// the element detaches mid-check (common on pages that re-render after a websocket push).
/// </summary>
/// <param name="locator">The element to wait for and click.</param>
/// <param name="timeoutMs">Max time to wait before failing, in milliseconds.</param>
public async Task ClickWhenStableAsync(ILocator locator, int timeoutMs = 5000)
{
    // ...
}

JSDoc

Same idea in JavaScript — types and defaults documented at the call site, not just in the implementation.

JavaScript
/**
 * Polls the given selector until its text matches `expected` or the timeout elapses.
 *
 * @param {string} selector - CSS selector for the element to poll.
 * @param {string} expected - Exact text content to wait for.
 * @param {number} [timeoutMs=5000] - How long to keep polling before throwing.
 * @returns {Promise<void>}
 */
async function waitForText(selector, expected, timeoutMs = 5000) {
  // ...
}

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.

Markdown
## 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.