AppTech Logo
Software Testing Strategies Top Tech Companies Use, With Code Examples

Software Testing Strategies Top Tech Companies Use, With Code Examples

SA
Shahbaz Ahmad
Web Developer
October 31, 20267 min read

The Art of Testing Code, and How Top Teams Do It

Top tech companies combine three things to ship fast and stay reliable: automated test layers, early defect prevention, and production monitoring. This article explains the thinking first, then each strategy in order, with a concrete example of how to do it.

Running example used throughout: a small checkout feature. It applies a discount code, calculates the total, and charges the card.

The Art of Testing Code

Testing is not "writing code to check code." It is the practice of turning assumptions about behavior into executable proof, so that you can change code without fear.

The Mindset

  1. One test, one promise.
    Each test should prove one thing: "If the user does X, the app does Y." For example, "Applying code SAVE10 to a $100 cart gives a $90 total." If one test checks five things, you won't know which one broke.
  2. Test what the code does, not how it does it.
    Check the result, not the internal steps. If you refactor the code and the behavior stays the same, your tests should still pass. A bad test checks that a helper function was called. A good test checks that the total is $90.
  3. You can't test everything, so protect what costs you most.
    Test hardest where a bug means lost money, a security hole, or lost user data, such as payments, login, and saving data. Don't spend hours testing a tooltip.
  4. Keep tests fast, or people will stop running them.
    If the test suite takes 20 minutes, developers will skip it. Aim for unit tests that finish in seconds and a full suite that finishes in minutes.
  5. Fix or delete flaky tests.
    A flaky test passes and fails randomly without any code change. Teams learn to ignore failing builds, and then a real bug slips through unnoticed.
  6. Write tests that explain the code.
    A new teammate should be able to read the test names and understand what the feature is supposed to do. For example, a test named "never returns a negative total" tells you the rule without opening the code.

Anatomy of a good test: Arrange, Act, Assert

anotomy·TypeScript
1// checkout.test.ts
2import { describe, it, expect } from "vitest";
3import { calculateTotal } from "./checkout";
4
5describe("calculateTotal", () => {
6  it("applies a 10% discount code to the subtotal", () => {
7    // Arrange: set up the input
8    const items = [{ price: 50, qty: 2 }];      // subtotal 100
9    const code = "SAVE10";
10
11    // Act: do the one thing under test
12    const total = calculateTotal(items, code);
13
14    // Assert: check the outcome
15    expect(total).toBe(90);
16  });
17});
18

What to test (the checklist)

  • Happy path: the normal, expected use.
  • Boundaries: 0, 1, max, empty, null, negative.
  • Failure paths: invalid input, network down, timeout, permission denied. Its called negative space programming.
  • Business rules: the rules the company would lose money or trust over.
negativetotal·TypeScript
1it("never returns a negative total", () => {
2  expect(calculateTotal([{ price: 5, qty: 1 }], "SAVE100")).toBe(0);
3});
4it("throws on an unknown discount code", () => {
5  expect(() => calculateTotal([{ price: 5, qty: 1 }], "FAKE")).toThrow();
6});
7it("returns 0 for an empty cart", () => {
8  expect(calculateTotal([], null)).toBe(0);
9});

Mocks, stubs, and fakes in one line each

  • Stub: returns a canned answer. ("Pretend the tax API returns 5%.")
  • Mock: also records that it was called, so you can verify the call.
  • Fake: a lightweight working replacement (an in-memory database).

Rule: mock at the boundary (network, database, clock), never your own business logic.

The Test Pyramid Architecture

Why a pyramid: lower layers are cheap and pinpoint failures; upper layers are realistic but slow and flaky. A healthy suite is roughly 70% unit, 20% integration, 10% end-to-end (a guideline, not a law).

Blog post image

pyramid testing

Layer 1: Unit tests (base)

Test one function or class in isolation. Done above with calculateTotal. They run in milliseconds.

Layer 2: Integration tests (middle)

Check that modules, the database, or services work together. Example: the checkout API with a real test database.

superset·TypeScript
1// checkout.api.test.ts
2import request from "supertest";
3import { app } from "./app";
4import { db } from "./testDb";
5
6beforeEach(async () => { await db.reset(); await db.seed({ code: "SAVE10", pct: 10 }); });
7
8it("POST /checkout stores an order with the discounted total", async () => {
9  const res = await request(app)
10    .post("/checkout")
11    .send({ items: [{ id: "p1", price: 50, qty: 2 }], code: "SAVE10" });
12
13  expect(res.status).toBe(201);
14  const order = await db.orders.findById(res.body.orderId);
15  expect(order.total).toBe(90);
16});

Layer 3: End-to-end / UI tests (peak)

Simulate a real user across the whole system. Keep these for critical journeys only.

playwrite·TypeScript
1// checkout.e2e.spec.ts (Playwright)
2import { test, expect } from "@playwright/test";
3
4test("user can buy an item with a discount code", async ({ page }) => {
5  await page.goto("/shop");
6  await page.getByRole("button", { name: "Add to cart" }).first().click();
7  await page.getByRole("link", { name: "Cart" }).click();
8  await page.getByLabel("Discount code").fill("SAVE10");
9  await page.getByRole("button", { name: "Pay" }).click();
10  await expect(page.getByText("Order confirmed")).toBeVisible();
11});
12

Common mistake: the "ice-cream cone" (mostly E2E, few unit tests). It is slow, flaky, and hard to debug.

Shift-Left Testing

Idea: find problems as early as possible in the software development lifecycle, when they are cheapest to fix. A bug found while typing costs minutes; the same bug found in production costs hours, money, and trust.

Blog post image

left sift curve


How to do it

a) Static analysis and linting (catches bugs without running code)

package.json·TypeScript
1// checkout.e2e.spec.ts (Playwright)
2import { test, expect } from "@playwright/test";
3
4test("user can buy an item with a discount code", async ({ page }) => {
5  await page.goto("/shop");
6  await page.getByRole("button", { name: "Add to cart" }).first().click();
7  await page.getByRole("link", { name: "Cart" }).click();
8  await page.getByLabel("Discount code").fill("SAVE10");
9  await page.getByRole("button", { name: "Pay" }).click();
10  await expect(page.getByText("Order confirmed")).toBeVisible();
11});
12

b) Pre-commit hooks (block bad code before it leaves your machine)

# .husky/pre-commit
npm run lint && npm run typecheck && npx vitest related --run

c) Code review checklist (a human shift-left step)

  • Is there a test for the new behavior and the failure case?
  • Any secrets, unvalidated input, or unsafe queries?

d) Security scan early

npm audit --audit-level=high

What this catches: type errors, null bugs, unused code, injection risks, leaked secrets, all before the code reaches staging.

Shift-Right and Production Monitoring

Idea: some problems only appear with real users, real data, and real scale. So you test safely in production too.

How to do it

a) Error tracking and telemetry

Code
1import * as Sentry from "@sentry/node";
2Sentry.init({ dsn: process.env.SENTRY_DSN, tracesSampleRate: 0.1 });
3
4try {
5  await chargeCard(order);
6} catch (err) {
7  Sentry.captureException(err, { tags: { feature: "checkout" } });
8  throw err;
9}
10

b) Feature flags + canary release (expose the new feature to a tiny slice of traffic)

Code
1if (flags.isEnabled("new-checkout", user.id)) {
2  return newCheckout(order);   // starts at 1%, then 5%, 25%, 100%
3}
4return oldCheckout(order);
5

c) Rollout rules written down in advance

StageTrafficPromote ifRoll back if
Canary1%Error rate within 10% of baselineError rate up 2x or payment failures up
Partial25%Latency p95 stableLatency p95 up 20%
Full100%24h cleanAny severity-1 alert

d) Dark launch: run the new code path silently alongside the old one, compare results, show users nothing.

e) Synthetic checks: a bot runs the checkout journey every 5 minutes against production and alerts on failure.

Continuous Integration and Continuous Testing (CI/CT)

Idea: every commit or pull request automatically builds the app and runs the regression suite. Nothing merges on red.

Blog post image

ci/ct

How to do it

test·YAML
1# .github/workflows/ci.yml
2name: CI
3on: [pull_request]
4
5jobs:
6  test:
7    runs-on: ubuntu-latest
8    steps:
9      - uses: actions/checkout@v4
10      - uses: actions/setup-node@v4
11        with: { node-version: 20, cache: npm }
12      - run: npm ci
13      - run: npm run lint && npm run typecheck
14      - run: npx vitest run --coverage
15      - run: npx playwright install --with-deps
16      - run: npx playwright test --grep @smoke
17

Enforce a coverage gate:

config·TypeScript
1// vitest.config.ts
2export default {
3  test: {
4    coverage: {
5      thresholds: { lines: 80, branches: 75 }  // build fails if lower
6    }
7  }
8};
9

Then protect the main branch: require the CI check to pass and one review before merge.

Good practice: run fast checks (lint, unit, smoke E2E) on every PR; run the slow ones (full E2E, cross-browser, load) nightly.

TDD and BDD

Blog post image

tdd

TDD: Test-Driven Development (Red, Green, Refactor)

Write a failing test first, make it pass with the simplest code, then clean up.

Red: write the test for a new rule (free shipping over $100). It fails.

free shipping·TypeScript
1it("gives free shipping when subtotal is over 100", () => {
2  expect(shippingCost(120)).toBe(0);
3  expect(shippingCost(80)).toBe(7);
4});
5

Green: write the minimum code to pass.

cost·TypeScript
1export const shippingCost = (subtotal: number) => (subtotal > 100 ? 0 : 7);

Refactor: improve names and structure, run the tests, they stay green.

Best for: logic-heavy code (pricing, parsing, rules). The test acts as your design spec.

BDD: Behavior-Driven Development (Given-When-Then)

Product, QA, and developers agree on behavior in plain language, and that text is executable.

discount-code·Markdown
1Feature: Discount codes
2  Scenario: Valid code reduces the total
3    Given a cart with 2 items priced 50 each
4    When the customer applies the code "SAVE10"
5    Then the order total is 90
6
7  Scenario: Invalid code is rejected
8    Given a cart with 1 item priced 50
9    When the customer applies the code "FAKE"
10    Then an error "Invalid code" is shown
11    And the total stays 50
12

Step definitions connect the sentences to code:

js·TypeScript
1Given("a cart with {int} items priced {int} each", (n, price) => { cart = makeCart(n, price); });
2When("the customer applies the code {string}", (code) => { result = applyCode(cart, code); });
3Then("the order total is {int}", (t) => { expect(result.total).toBe(t); });
4

Best for: business-critical flows where non-engineers must confirm the rules. Don't BDD everything; the overhead is real.

Risk-Based and Exploratory Testing

Blog post image

risk

Risk-based testing

Spend testing effort in proportion to impact × likelihood of failure.

AreaImpact if brokenLikelihoodTest investment
Payment processingVery high (lost revenue, legal)MediumHeavy: unit + integration + E2E + monitoring
Login / authVery high (security)MediumHeavy
Profile photo uploadMediumMediumModerate
Footer link colorLowLowMinimal / visual check only

How to do it: list features, score impact (1 to 5) and likelihood (1 to 5), multiply, and test from the highest score down. Revisit the table every release.

Exploratory testing

A human investigates the product without a script, using curiosity to find what automation never thought to check.

How to do it (a time-boxed session):

time boxed·Markdown
1Charter:  Explore the checkout with unusual inputs to find calculation or payment errors.
2Time box: 60 minutes
3Ideas:    - emoji and very long text in the discount field
4          - double-click Pay quickly
5          - change cart in a second tab mid-payment
6          - lose network after pressing Pay
7          - use an expired card, then a valid one
8Notes:    Record bugs, questions, and areas still unexplored.
9

Every bug found this way should become an automated test, so it never returns.

Crowdtesting: pay external testers on many real devices and networks to find issues your team's setup can't reproduce.

Putting It All Together

StageStrategyExample in the checkout feature
Writing codeTDD, shift-leftFailing test first, lint and type-check on commit
Opening a PRCI/CTLint, unit, integration, smoke E2E, coverage gate
Before releasePyramid, risk-based, exploratoryFull E2E on payment, a 60-minute exploratory session
ReleasingShift-rightFeature flag, 1% canary, rollback rules
In productionMonitoringSentry errors, synthetic checkout check every 5 minutes
Feedback loopAllEvery production bug becomes a new automated test

One-page summary

  1. Tests turn assumptions into proof; test behavior, not implementation.
  2. Many fast unit tests, fewer integration tests, very few E2E tests.
  3. Catch problems early (shift-left), and verify safely in production (shift-right).
  4. Automate it all in CI and block merges on failure.
  5. TDD for logic, BDD for shared business rules.
  6. Focus effort on what hurts most, and keep humans exploring for what you didn't think of.

References

  1. https://testmatick.com/integration-testing-strategies-used-by-top-10-software-testing-companies/
  2. https://testcollab.com/blog/software-testing-strategies
  3. https://testomat.io/blog/software-testing-strategies-types-elements-and-best-practices/
  4. https://momentic.ai/blog/enterprise-testing-strategy-guide
  5. https://www.youtube.com/watch?v=5b71qDvrGDU&t=47
  6. https://group107.com/blog/software-testing-best-practices/