top of page

Testing Fundamentals: Why We Test

  • Shawn West
  • Dec 21, 2025
  • 6 min read

Updated: Aug 6

Testing is the most argued-about, least understood practice in software. Teams fight over coverage percentages. Managers ask whether testing is "done," as if it were a phase with an end. Developers skip it to hit a date and then donate their weekend to production.

All of that comes from one wrong frame: testing as a chore you perform because a process demands it. Flip the frame and everything clarifies. Tests aren't a tax on shipping — they're the thing that lets you ship quickly without flinching. A good suite doesn't slow you down; it's what makes speed safe.

This post covers why we actually test, the types that matter and when, and why the usual objection — "no time" — is the argument that costs the most.

Why We Test

Four reasons, none of which is "the process says so."

Confidence

Every change carries risk: the new feature that breaks an old one, the fix in module A that trips a failure in module C nobody remembered was connected. Without tests, each deploy is a bet placed blind. With them, you get evidence — not proof, evidence — that the system still does what it did yesterday.

That evidence changes behavior more than any policy does. The team with a real suite deploys at 2 p.m. on a Tuesday and goes home. The team without one deploys Friday night with everyone on the bridge call, because the only way they'll find out it broke is when a customer tells them. One of those teams ships faster, and it isn't the one eating pizza at midnight.

Living Documentation

Tests describe what the software is supposed to do — not in a wiki last touched eight months ago, not in a comment that drifted out of sync three releases back, but in statements that fail the moment reality stops matching the description. "Given this input, expect this output" is a specification that checks itself against the code on every run. Nothing else you write does that.

Regression Prevention

Software is layered and the layers reach into each other in ways no one holds in their head. You ship a login tweak and the checkout flow quietly breaks; the link between them is invisible until support's phone rings. Regression tests — the existing suite, re-run on every change — surface that link in the build, where it costs a red pipeline, instead of in production, where it costs a customer.

Faster Development — the one people don't believe

This is the counterintuitive one and the most important. Tests speed development up, through two mechanisms most people underrate.

Debugging: without a test, a bug is logs, manual repro, and guessing. With a failing test, it's a precise, repeatable statement of exactly what's wrong — fix, re-run, confirmed in seconds. Refactoring: without tests, restructuring is a gamble you can't check, so you don't do it, and the code rots until every change fights you. With tests, you change the structure, run the suite, and know in a minute whether behavior held. The codebase stays malleable. That's where the speed comes from — not typing faster, but never being afraid to touch your own code.

The Types of Testing

Different tests catch different failures at different costs. Spend accordingly.

Unit Tests

A unit test checks one function or component in isolation: does this piece do its job on its own? Say you have a discount calculator:

def apply_discount(price, discount_percent):
    if discount_percent < 0 or discount_percent > 100:
        raise ValueError("Discount must be between 0 and 100")
    return price * (1 - discount_percent / 100)

The tests pin the expected behavior and the edges:

def test_apply_discount_standard():
    assert apply_discount(100, 20) == 80.0

def test_apply_discount_zero():
    assert apply_discount(100, 0) == 100.0

def test_apply_discount_full():
    assert apply_discount(100, 100) == 0.0

def test_apply_discount_invalid():
    with pytest.raises(ValueError):
        apply_discount(100, -5)

Four tests, each running in milliseconds, each documenting one behavior. Change apply_discount and break any of them, and you know instantly — no manual checking, no waiting for QA. Unit tests are fast, cheap, and precise, which is why they're the base of any sane strategy.

Integration Tests

A function can pass every unit test in isolation and still fail the moment it talks to the real database, the API layer, or another service. Integration tests answer a different question — do these pieces actually fit together? — and catch the whole category unit tests can't: miscommunication between components. They're slower because they touch real dependencies, which is exactly why you write fewer of them and aim them at the seams.

End-to-End Tests (E2E)

E2E tests drive the whole system the way a user would: log in, navigate, submit, verify. They're the most expensive to write, the slowest to run, and — be honest about this going in — the flakiest; a passing E2E suite that fails randomly twice a week trains your team to ignore red, which is worse than having no suite at all. But they're also the only tests that prove the individually-correct pieces actually assemble into a working product. Keep them few and aimed at the paths that make you money or lose you customers.

That ordering — many unit, fewer integration, a handful of E2E — is the testing pyramid, and it's shaped that way because every level up costs more and runs slower. Invest at the bottom.

Manual vs. Automated Testing

Automation owns the repetitive, deterministic checks: right return value, right status code, workflow completes clean. Machines beat humans here — they don't get bored, don't skip steps, and run the same check at 2 a.m. on a holiday. Humans own the subjective and exploratory: does this feel confusing, what breaks if a user does something no one anticipated. The failure mode is treating it as either/or. Automate everything expressible as pass/fail; point your people at exploration and usability, where a curious mind finds what no one thought to assert.

The Cost Curve

The economic case is real, but state it honestly — because the version you usually hear is half folklore.

The direction is rock-solid: Barry Boehm documented in 1981 that the cost to fix a defect climbs by roughly an order of magnitude as it slips from design to code to production, and NIST's 2002 study put the national drag from inadequate testing at about $59.5 billion a year. The tidy numbers — "1x in dev, 10x in QA, 100x in prod" — get quoted with a confidence the underlying data never earned. So don't sell the 100x; sell the shape, which is steep and not in dispute.

You can feel the curve without a chart. A bug caught while you're writing the code costs minutes — you're already in the file with the intent loaded. The same bug caught in QA costs hours — someone reproduces it, files it, and you reload context you'd dumped. Caught in production, the fix might still be a ten-minute change, but it now arrives wrapped in incident response, customer comms, and a post-mortem. The defect never changed. Only when you found it did. (This is the whole argument for shifting left.)

"I Don't Have Time to Test"

The most common objection, and precisely backwards. "No time to test" doesn't save the time — it relocates it to the worst possible place: after deploy, as production debugging, manual re-verification of every release, and outage explanations to people who sign your checks.

It's a cycle, and it accelerates: skip tests to ship faster → hit bugs in production → firefight instead of build → fall behind → skip more tests to catch up. It does not resolve on its own. Breaking it costs one deliberate investment: write a test for the next bug you fix, then the next feature you build. In weeks the net starts to hold; in a couple of months the team is moving faster than it did untested, because the hours that went to manual checking and firefighting go to building instead. The real question was never "do I have time to test" — it's "how much am I already losing because I don't?"

Key Takeaway

Testing buys confidence, speed, and documentation in one purchase. It isn't overhead and it isn't a luxury for teams with slack — it's the line between shipping with control and shipping on hope.

Practically: build a broad base of unit tests, add integration tests at the seams, keep a few E2E tests on the paths that matter, automate every pass/fail check, and spend your humans where judgment beats assertions. The cost of testing is visible and up front; the cost of skipping it is hidden and compounds. You get to choose which kind of cost you'd rather carry.

Next in this learning path: Shifting Left — Building Quality Into Every Phase of Development — how quality practices move upstream into every stage of the SDLC, from requirements through production.

Related reading

Keep learning. This article is part of the Software Testing Foundations path in the ShiftQuality Learning Center. Learn to design tests that catch real bugs.

bottom of page