A Correct-Looking Algorithm Can Still Be Wrong
Suppose we want to return the larger of two numbers:
IF a > b
RETURN a
ELSE
RETURN a
END IF
The structure looks complete, but both branches return \(a\).
Testing with
\[ (a,b)=(8,3) \]does not reveal the problem because the output \(8\) happens to be correct.
Testing with
\[ (a,b)=(3,8) \]does reveal it:
\[ \text{expected}=8, \qquad \text{actual}=3. \]A useful test is therefore not merely an input. It includes an expected behavior.
\[ \boxed{ \text{test} = \text{input} + \text{expected result} + \text{comparison} } \]What Is Testing?
Testing evaluates an algorithm on selected inputs and checks whether the observed behavior matches the expected behavior.
Testing may examine:
- final outputs,
- intermediate states,
- error messages,
- termination,
- performance,
- required conditions or invariants.
The purpose is to gather evidence about whether the algorithm behaves correctly over the intended problem domain.
Test Cases
A test case is a selected input together with the expected result or behavior.
For a function
\[ f(x)=2x+1, \]a test case might be:
\[ x=4, \qquad \text{expected output}=9. \]A table helps organize several cases:
| Input | Expected Output | Actual Output | Pass? |
|---|---|---|---|
| \(0\) | \(1\) | \(1\) | Yes |
| \(4\) | \(9\) | \(9\) | Yes |
| \(-3\) | \(-5\) | \(-5\) | Yes |
Normal Cases
A normal case is an ordinary input well inside the expected domain.
For an algorithm computing
\[ \frac{a}{b}, \]a normal test might be
\[ a=12, \qquad b=3. \]The expected output is
\[ \boxed4. \]Normal cases check whether the main path of the algorithm works as intended.
Boundary Cases
A boundary case lies at or near an important limit in the problem.
Boundary tests are especially valuable because inequalities and loop limits often fail there.
Example
Suppose a rule accepts values satisfying
\[ 0\leq x\leq100. \]Important tests include
\[ x=0, \qquad x=100, \]and often nearby values such as
\[ x=-1, \qquad x=101. \]These cases reveal mistakes involving \(<\) versus \(\leq\).
Invalid and Exceptional Cases
A robust algorithm should also be tested with inputs that violate its preconditions.
For division,
\[ \frac{a}{b}, \]the case
\[ b=0 \]should not be treated like an ordinary valid input.
The expected behavior might be:
- reject the input,
- return an error message,
- request a replacement value.
Testing invalid input checks whether the algorithm protects its own domain.
Expected versus Actual Results
Testing depends on comparison.
\[ \boxed{ \text{error signal} = \text{actual behavior} \neq \text{expected behavior} } \]The expected result should come from:
- a known mathematical result,
- a hand calculation,
- a trusted reference method,
- a stated specification.
If the expected result itself is wrong, the test is not reliable.
The Test-Debug-Retest Cycle
What Is Debugging?
Debugging is the process of finding, understanding, and correcting the cause of incorrect behavior.
A useful debugging question is not merely:
“Why is the answer wrong?”
It is:
“At what step does the actual state first differ from the expected state?”
That question turns a large failure into a smaller local problem.
Tracing State
Trace tables are one of the most effective debugging tools for simple algorithms.
Consider:
total = 0
FOR i = 1 TO 4
total = total + 2
END FOR
Expected final total:
\[ 8. \]| Iteration | \(i\) | total |
|---|---|---|
| start | — | 0 |
| 1 | 1 | 2 |
| 2 | 2 | 4 |
| 3 | 3 | 6 |
| 4 | 4 | 8 |
If the actual trace differs at iteration \(3\), the problem is likely near the third update rather than somewhere vaguely “in the loop.”
Kinds of Errors
Specification Error
The algorithm solves the wrong problem because the requirements were misunderstood.
Logic Error
The steps execute but produce an incorrect result.
Boundary Error
A rule fails at an endpoint or transition case.
Input Error
Invalid or malformed data enters the process without proper handling.
Arithmetic Error
A numerical operation or formula is entered incorrectly.
Termination Error
The process fails to stop, stops too early, or stops for the wrong reason.
In an actual programming language, there can also be syntax or runtime errors. At the algorithmic level, the categories above are more general.
Assertions and Checks
An assertion is a condition that is expected to be true at a particular point in a process.
Example
If an algorithm is computing a probability, we may assert:
\[ 0\leq p\leq1. \]If the result is
\[ p=1.4, \]the assertion fails and signals a problem.
Assertions turn mathematical expectations into error-detection rules.
Invariants as Error Detectors
An invariant is a property expected to remain true throughout a process.
Example: Running Count
Suppose an algorithm has processed exactly \(k\) values. Then we may expect:
\[ \text{count}=k. \]If a trace shows
\[ k=7 \]but
\[ \text{count}=6, \]the invariant has been violated.
Invariants are useful both for proofs of correctness and for debugging.
Independent and Redundant Checks
Important computations are often checked in more than one way.
Example: Rectangle Area
Suppose a spreadsheet reports
\[ A=12(8)=96. \]An independent mental estimate
\[ 10(8)\approx80 \]makes \(96\) plausible.
For higher-stakes work, a second formula, alternate algorithm, or independent calculation may provide a stronger check.
\[ \boxed{ \text{independent agreement} \text{ provides stronger evidence than repeated use of the same method} } \]Testing Properties, Not Just Examples
Instead of checking only a few exact outputs, we can also test general properties.
Example: Absolute Value
For every real input, a correct absolute-value algorithm should satisfy
\[ |x|\geq0. \]It should also satisfy
\[ |-x|=|x|. \]These are properties that can be tested across many inputs.
Property-based tests are especially useful when the exact output is difficult to compute independently but a general mathematical relationship is known.
Regression Testing
When a bug is corrected, the failing input should often be saved as a future test.
Suppose an algorithm once failed at
\[ x=0. \]After fixing the zero case, keep \(x=0\) in the test set.
A regression test checks that later changes do not reintroduce an error that was already fixed.
What Testing Cannot Prove
Passing many tests is evidence of correctness, but it is not automatically a proof.
A program can pass
\[ 1000 \]test cases and still fail on the \(1001\)st.
For finite domains, exhaustive testing may be possible. For large or infinite domains, formal reasoning is often needed to establish correctness for every valid input.
\[ \boxed{ \text{testing can show the presence of errors;} \quad \text{passing tests alone may not prove their absence} } \]A Practical Debugging Workflow
- Reproduce the failure. Find a specific input that reliably causes it.
- State the expected result. Know what should happen before investigating.
- Trace intermediate state. Locate the first incorrect value or branch.
- Isolate the smallest cause. Narrow the problem to one rule, condition, or update.
- Correct the cause. Avoid merely hiding the visible symptom.
- Retest the failing case. Confirm the correction works.
- Run related tests. Make sure the correction did not break another case.
- Keep the case as a regression test.
Practical Applications
Software Development
Test suites check functions, interfaces, error handling, and later changes.
Spreadsheets
Check cells, balance identities, and representative row formulas help detect reference mistakes.
Numerical Methods
Residuals, error bounds, and convergence checks help detect failed approximations.
Engineering
Independent calculations and tolerance checks help identify design or measurement problems.
Mathematics
Counterexamples test conjectures, while proof establishes claims over an entire domain.
Worked Examples
Example 1: Boundary Bug
An algorithm should accept ages \(18\) and above:
IF age > 18
output "eligible"
ELSE
output "not eligible"
END IF
Test:
\[ age=18. \]Expected:
\[ \text{eligible}. \]Actual:
\[ \text{not eligible}. \]The bug is the boundary operator. The condition should be
\[ \boxed{age\geq18}. \]Example 2: Wrong Accumulator Start
Suppose we want the product
\[ 2\cdot3\cdot4. \]But the algorithm starts:
product = 0
FOR each x
product = product * x
END FOR
Every result remains \(0\).
The multiplicative accumulator should start at
\[ \boxed1. \]Example 3: Missing Invalid Input Check
An average algorithm receives an empty list.
Then
\[ n=0, \]so the formula
\[ \frac{\text{sum}}{n} \]attempts division by zero.
A test with the empty list reveals that a guard condition is required.
Example 4: Trace a Logic Error
x = 5
y = x + 2
x = y + 1
y = x * 2
| After Step | \(x\) | \(y\) |
|---|---|---|
| start | 5 | — |
| \(y=x+2\) | 5 | 7 |
| \(x=y+1\) | 8 | 7 |
| \(y=2x\) | 8 | 16 |
If someone expected \(y=14\), the trace shows the expectation was based on an older value of \(x\). The algorithm may be correct; the expected result may be wrong.
Example 5: Property Test
For a sorting algorithm, every output list should satisfy:
\[ a_1\leq a_2\leq\cdots\leq a_n. \]It should also contain the same elements as the input.
These two properties can detect many sorting failures without knowing the exact output in advance for every test.
Example 6: Regression Test
A formula once failed when
\[ x=0. \]After correcting the zero case, add \(x=0\) permanently to the test set so future changes are checked against the known failure mode.
Common Errors
1. Testing Only Easy Inputs
Normal cases may miss boundary, invalid, or rare paths.
2. Using the Same Incorrect Method to Compute the Expected Answer
A test is weaker if the expected result repeats the same mistake as the algorithm under test.
3. Fixing the Symptom Instead of the Cause
A special-case patch may hide one failure while leaving the underlying logic wrong.
4. Changing Several Things at Once
Large simultaneous changes make it harder to identify which correction actually solved the problem.
5. Forgetting to Retest Earlier Cases
A fix for one input can accidentally break another.
6. Treating a Passing Test Suite as a Mathematical Proof
Testing provides evidence. Proof establishes a claim for an entire stated domain.
7. Ignoring Unexpected but Plausible Outputs
Not every error produces an obviously absurd number. Comparison with an expected result is still necessary.
Practice
Boundary Test
A rule accepts
\[ x\geq10. \]Which input is especially important to test?
Check Answer
\[ \boxed{x=10} \]because it lies exactly on the boundary.
Invalid Input
What special case should be tested for an algorithm computing \(a/b\)?
Check Answer
\[ \boxed{b=0}. \]Accumulator Bug
A sum accumulator is initialized to \(1\) instead of \(0\). What kind of error is this most naturally?
Check Answer
A logic/initialization error. Every computed sum will be shifted by \(1\).
Assertion
A computed probability is \(1.2\). Which assertion detects the error?
Check Answer
\[ \boxed{0\leq p\leq1}. \]Testing versus Proof
If an algorithm passes \(10{,}000\) tests over an infinite input domain, has correctness been proved?
Check Answer
No. The tests provide strong evidence, but a proof or other exhaustive argument is needed to establish correctness over the whole infinite domain.
The Deeper Idea
Testing and debugging make algorithmic reasoning empirical.
We begin with a specification:
\[ \text{input} \longmapsto \text{expected behavior}. \]Then we compare it with execution:
\[ \text{input} \longmapsto \text{actual behavior}. \]A mismatch gives us a counterexample to the claim that the algorithm is correct for that input.
This creates a strong connection with mathematical reasoning:
- test cases are examples,
- failing tests are counterexamples,
- assertions are propositions about state,
- invariants express persistent structure,
- formal proof generalizes beyond selected examples.
Testing therefore does more than “check code.” It teaches a general habit: make expectations explicit, compare them with evidence, and investigate the first place where the two diverge.
Summary
- Testing compares actual algorithm behavior with expected behavior on selected inputs.
- A test case includes an input and an expected result or outcome.
- Normal cases check ordinary behavior.
- Boundary cases test endpoints and transition values.
- Invalid and exceptional cases test whether preconditions are enforced.
- Debugging finds and corrects the cause of incorrect behavior.
- Trace tables help locate the first state that differs from expectation.
- Errors may arise from the specification, logic, boundaries, input handling, arithmetic, or termination behavior.
- Assertions encode conditions that should be true at specific points.
- Invariants can detect violations of expected structure during execution.
- Independent checks are stronger than repeating the same computation.
- Property-based tests check general mathematical relationships across many inputs.
- Regression tests preserve previously failing cases so bugs do not quietly return.
- Passing tests provides evidence of correctness but does not always prove correctness over the full domain.
- A disciplined debugging workflow reproduces, traces, isolates, corrects, and retests a failure.
Reliable computation depends on more than writing an algorithm that appears reasonable. We need ways to challenge it, observe it, detect when expectations fail, and trace those failures back to their cause. Testing finds the evidence; debugging explains it; error detection helps catch the problem sooner.
Algorithms and Computational Thinking
- Previous: Termination and Stopping Conditions
- Algorithms and Computational Thinking — return to the lesson directory.