A basic meaningful test set covers three categories: expected use, important boundaries, and invalid input. This does not prove a program correct, but it tests more than one friendly example.
Begin with a specification
Suppose a function calculates the mean of a non-empty list of scores from 0 through 100. It returns a numeric mean and rejects an empty list, nonnumeric values, and values outside the range.
The specification defines the tests. Without it, you may only confirm whatever the current code happens to do.
Test expected behavior
Expected cases represent ordinary valid use:
[80, 90, 100] → 90
[50] → 50
Use values whose result you can calculate independently. One expected case is rarely enough when different valid shapes follow different paths.
Test boundaries
Boundaries sit at or near a rule’s edge:
[0] → 0
[100] → 100
[0, 100] → 50
Other boundaries include empty versus one item, maximum length, dates around a month change, and text at an allowed size. Choose boundaries from requirements, not from a generic list.
Test invalid input
Invalid tests confirm rejection behavior:
[] → clear empty-input error
[101] → clear range error
[-1] → clear range error
["ninety"] → clear type or format error
State what safe failure means. A crash, silent correction, partial output, or exposure of private data is not acceptable merely because the input was invalid.
Build a test table
| Category | Input | Expected result | Reason |
|---|---|---|---|
| Expected | [80,90,100] |
90 |
Ordinary values |
| Boundary | [0,100] |
50 |
Both valid extremes |
| Invalid | [] |
Empty-input error | Required non-empty |
| Invalid | [101] |
Range error | Above maximum |
Run each case and record actual output. Do not mark a test passed without executing it. If an AI generates candidate tests, compare them with the specification and calculate expected results independently.
Add regression tests
When debugging reveals a defect, preserve a minimal test that fails before the fix and passes after it. The test should express expected behavior, not depend on an accidental implementation detail.
Consider privacy and side effects
Use synthetic data unless authorized real data is necessary. For programs that write files, send messages, charge accounts, or change systems, use a practice environment, temporary destination, mock, or dry run appropriate to the project. Verify cleanup and do not test destructive behavior on valuable data.
Know the limits
Three categories are a starting framework, not full coverage. Security, performance, concurrency, accessibility, numerical precision, and integration may require specialized tests. State what the current set does not assess.
Common mistakes
- Writing expected outputs after running the program.
- Testing only examples used during implementation.
- Calling an empty input invalid when the specification permits it.
- Checking that an error occurred but not which error.
- Using private production data for convenience.
- Accepting generated tests without inspecting assumptions.
Do this now
Implement or use a safe score-mean function. Write at least two expected, three boundary, and four invalid cases before changing the implementation. Run them and record actual results.
Log what you learned
Record only:
- Result: What did the action produce?
- Evidence: What observation, test, or source supports that result?
- Next action or unresolved question: What should happen next?
Next, package this evidence into a reproducible bug report when another person needs to help.