Testing Programs
"It Works On My Machine"
You wrote the program. You ran it once with easy numbers. It worked. Ship it?
Every programmer learns — usually the embarrassing way — that "I tried it once" and "it works" are different claims. Programs meet messy reality: users type words where numbers should go, enter 0 where you assumed at least 1, and find the one path through your nested IFs you never tried. Testing is how you meet reality first: deliberately probing your program with chosen inputs and comparing against what should happen.
Testing and debugging are partners with different jobs: testing finds the failures; debugging explains and fixes them.
The Three Kinds of Test Data
Suppose a sign-up form must accept ages 13 to 19. Testing it with 15 alone is barely testing at all. Professionals choose from three categories:
Normal data — sensible, valid input. 15, 17. The program should accept and process it. This confirms the happy path works.
Boundary data — values exactly at the edges of the rules: 13 and 19 (just valid), 12 and 20 (just invalid). Boundaries are where bugs breed, because edges are where < gets confused with <= — the same off-by-one villain from loops. If you test only four values in your life, make them the two edges and their two neighbours.
Erroneous data — input that is plain invalid: "hello", -5, an empty box. The program should reject it gracefully — an error message and another chance, not a crash. A program that dies on bad input fails this test, even though the input was "wrong": real users mistype constantly, and handling that is part of the program's job.
The Test Plan
Testing is only convincing when it is written down. A test plan is a table designed before the testing starts:
| Test | Input | Type | Expected result | Actual result | Pass? |
|---|---|---|---|---|---|
| 1 | 15 | normal | accepted | accepted | ✓ |
| 2 | 13 | boundary | accepted | accepted | ✓ |
| 3 | 12 | boundary | rejected with message | accepted! | ✗ |
| 4 | "hello" | erroneous | rejected with message | rejected | ✓ |
The crucial discipline: fill in "expected result" before running anything. Decide it afterwards and you fall into the oldest trap — the output looks plausible, so you nod it through. Predicting first (with a trace table, if the logic is subtle) turns each row into an honest check.
And look — test 3 failed. Age 12 slipped in. Somewhere a condition says >= 12 or > 11 where it should say >= 13. The test plan didn't just find a bug; its boundary row pointed at exactly the kind of bug it is. Now debugging is short.
Test the Pieces, Then the Whole
Testing a big program all at once is like tasting a finished cake to find out which ingredient was bad. Better: test parts alone. If your code is built from functions, each can be probed directly — feed larger(4, 9) every kind of data and confirm the answers before anything depends on it. Professionals automate exactly this ("unit tests"): programs that test programs, re-run after every change.
That re-running matters more than beginners expect. Code changes constantly, and a change here can silently break something there. Re-running old tests after changes — regression testing — catches yesterday's feature broken by today's fix.
What Testing Cannot Do
One honest limit, worth engraving: testing can show the presence of bugs, never their absence. Passing 50 tests means the program works on those 50 cases — not on the untried ones. This is not a reason to skip testing; it is the reason test choice matters. Fifty thoughtful tests spanning normal, boundary, and erroneous data build real confidence. Fifty friendly tests of the happy path build false confidence, which is worse than none.
Try It Yourself
A program awards grades: 0–39 fail, 40–69 pass, 70–100 distinction. Design a test plan of about ten rows covering all three data types. (Did you include 39, 40, 69, 70, 0, 100 — and 101, and −1, and "ninety"?) Which single input do you predict is most likely to expose a bug?
Worked Example — A Test Plan That Actually Finds a Bug
Here is a complete, realistic test plan for a program accepting a percentage discount code between 0 and 50:
| Test | Input | Type | Expected result | Actual result | Pass? |
|---|---|---|---|---|---|
| 1 | 25 | normal | accepted, 25% applied | accepted, 25% applied | ✓ |
| 2 | 0 | boundary | accepted, 0% applied | accepted, 0% applied | ✓ |
| 3 | 50 | boundary | accepted, 50% applied | error: "invalid discount" | ✗ |
| 4 | -1 | boundary | rejected with message | rejected with message | ✓ |
| 5 | 51 | boundary | rejected with message | rejected with message | ✓ |
| 6 | "fifty" | erroneous | rejected with message | rejected with message | ✓ |
Five tests pass and one fails — and it is a boundary test, exactly where this lesson predicted bugs concentrate. The likely cause, findable without even seeing the code: somewhere a condition reads discount < 50 where it should read discount <= 50, excluding the one value that should be the maximum allowed, not just under it. This single failing row demonstrates the whole argument of this lesson in miniature: testing only "normal" data (test 1) would have shipped this bug straight to real customers, and only the deliberate boundary-hunting habit — testing exactly at 50, not just comfortably below it — caught it before anyone else did.
Key Words
- Testing — checking a program against expected results with chosen inputs
- Normal / boundary / erroneous data — valid input / edge-of-rule input / invalid input
- Test plan — the table of inputs, expected results, and outcomes, designed in advance
- Regression testing — re-running old tests after code changes
- Graceful rejection — handling bad input with a message, not a crash