
Software Testing Strategies Top Tech Companies Use, With Code Examples
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
- 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. - 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. - 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. - 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. - 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. - 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
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});
18What 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.
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).

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.
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.
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});
12Common 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.

left sift curve
How to do it
a) Static analysis and linting (catches bugs without running code)
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});
12b) 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
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}
10b) Feature flags + canary release (expose the new feature to a tiny slice of traffic)
1if (flags.isEnabled("new-checkout", user.id)) {
2 return newCheckout(order); // starts at 1%, then 5%, 25%, 100%
3}
4return oldCheckout(order);
5c) Rollout rules written down in advance
| Stage | Traffic | Promote if | Roll back if |
|---|---|---|---|
| Canary | 1% | Error rate within 10% of baseline | Error rate up 2x or payment failures up |
| Partial | 25% | Latency p95 stable | Latency p95 up 20% |
| Full | 100% | 24h clean | Any 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.

ci/ct
How to do it
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
17Enforce a coverage gate:
1// vitest.config.ts
2export default {
3 test: {
4 coverage: {
5 thresholds: { lines: 80, branches: 75 } // build fails if lower
6 }
7 }
8};
9Then 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

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.
1it("gives free shipping when subtotal is over 100", () => {
2 expect(shippingCost(120)).toBe(0);
3 expect(shippingCost(80)).toBe(7);
4});
5Green: write the minimum code to pass.
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.
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
12Step definitions connect the sentences to code:
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); });
4Best for: business-critical flows where non-engineers must confirm the rules. Don't BDD everything; the overhead is real.
Risk-Based and Exploratory Testing

risk
Risk-based testing
Spend testing effort in proportion to impact × likelihood of failure.
| Area | Impact if broken | Likelihood | Test investment |
|---|---|---|---|
| Payment processing | Very high (lost revenue, legal) | Medium | Heavy: unit + integration + E2E + monitoring |
| Login / auth | Very high (security) | Medium | Heavy |
| Profile photo upload | Medium | Medium | Moderate |
| Footer link color | Low | Low | Minimal / 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):
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.
9Every 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
| Stage | Strategy | Example in the checkout feature |
|---|---|---|
| Writing code | TDD, shift-left | Failing test first, lint and type-check on commit |
| Opening a PR | CI/CT | Lint, unit, integration, smoke E2E, coverage gate |
| Before release | Pyramid, risk-based, exploratory | Full E2E on payment, a 60-minute exploratory session |
| Releasing | Shift-right | Feature flag, 1% canary, rollback rules |
| In production | Monitoring | Sentry errors, synthetic checkout check every 5 minutes |
| Feedback loop | All | Every production bug becomes a new automated test |
One-page summary
- Tests turn assumptions into proof; test behavior, not implementation.
- Many fast unit tests, fewer integration tests, very few E2E tests.
- Catch problems early (shift-left), and verify safely in production (shift-right).
- Automate it all in CI and block merges on failure.
- TDD for logic, BDD for shared business rules.
- Focus effort on what hurts most, and keep humans exploring for what you didn't think of.
References
- https://testmatick.com/integration-testing-strategies-used-by-top-10-software-testing-companies/
- https://testcollab.com/blog/software-testing-strategies
- https://testomat.io/blog/software-testing-strategies-types-elements-and-best-practices/
- https://momentic.ai/blog/enterprise-testing-strategy-guide
- https://www.youtube.com/watch?v=5b71qDvrGDU&t=47
- https://group107.com/blog/software-testing-best-practices/
