Testing Versus Proving Program Behavior
A program is correct when it produces the correct result as specified, so correctness is always measured against a specification.
Correct Compared to What
Many beginners believe that once a program has been shown to be correct, the job is finished and the program can be trusted to do what its users need. The belief sounds reasonable, but it quietly merges two different questions. The first question is whether the program does what it was asked to do. The second is whether what it was asked to do was the right thing in the first place. This article separates those questions, because the difference decides what a correctness proof can and cannot give you.
What do you think happens?
A program passes every test you wrote for it. Has it been shown to do what its users need?
Reveal answer
No, passing tests does not settle whether the program meets the real need
Tests show behavior on the cases you chose, and a program can match its specification while the specification itself is wrong. Keeping these questions apart is the starting point for understanding correctness.
What Provably Correct Means
A program is correct when it produces the correct result as specified. Correctness is therefore always measured against a specification: a statement of what the correct result must be. Without a specification there is nothing to measure the program against.
In some cases it is possible to demonstrate, mathematically, that the sequence of computations a program describes will always produce the correct result. This is the sense in which a program can be provably correct. The proof reasons about every possible run of the program, not just the few runs someone happened to try. That is what makes it stronger than testing alone.
Think of the specification as the target and the program as the thing being aimed. A program is provably correct when a proof shows that, for every possible run, the final result hits the target. The specification sits outside the program. The program can be faithful to every step it takes and still miss the target if the target was set wrongly.
Process and State in Execution
The reasoning behind correctness proofs rests on two ideas: process and state. The state of a program is all the information relevant at one moment. A process is the sequence of states the computer passes through during execution. Each statement that runs moves the computation from one state to the next, so a run of a program is a chain of states.
Tracing the states of a three-line program
Trace the states of this program, where each line is one statement: a = 2, then b = a * 3, then a = a + 1. Start from a state with no variables.
- 1State 0 (before any statement)
No variables exist yet, so the state holds no information.
- 2After a = 2
The state now records a as 2. Nothing else has changed.
- 3After b = a * 3
The right side is evaluated using the current state, where a is 2, giving 6. The state now records a as 2 and b as 6.
- 4After a = a + 1
The right side uses the current a, which is 2, giving 3. The state now records a as 3 and b as 6. The value of b did not change, because nothing re-ran the statement that set it.
The process is the sequence of four states, and the final state holds a as 3 and b as 6.
A Statement as a Change of State
The meaning of a statement can be expressed as its effect on the computer's state. This matters because that effect is a fact you can rely on when building a proof. To reason about a statement, ask two things: what changes in the state after the statement runs, and what stays the same. Everything the statement does not touch carries over unchanged from before to after.
State after the last line: x is 4, y is 7- Lines one and two set the state to x is 3 and y is 7.
- The third line reads the current x (3), adds one, and stores 4 back into x.
- y is never mentioned by the third line, so its value carries over unchanged.
The diagram shows the pattern. Only x changes, because the statement assigns to x. The value of y is the same before and after. A proof chains together facts like this one, each describing how a single statement moves the state, until it can say what the final state of every run must be.
Testing Samples, Proving Covers Every Run
Testing checks a program on some runs. Proving reasons about every possible run. The contrast is about coverage. A set of tests can show that a program behaves correctly on the cases tested, but the tests are only a sample. A proof is an argument about all runs at once, which is why it is stronger than testing alone.
| Question | Testing | Proving |
|---|---|---|
| What is examined | Some chosen runs of the program | The program's behavior on every possible run |
| What it establishes | Correct behavior on the cases tested | Final result matches the specification for every run, if the proof holds |
| Main limit | Silent about inputs nobody tried | Hard to carry out, and only as trustworthy as the specification |
| Typical scale | Can be applied to many kinds of programs | Full proofs are typically limited to fairly small programs |
Testing and proving answer different questions about program behavior.
Why Full Proofs Stay Small
Rigorous proof is genuinely hard. In practice, full mathematical proofs of correctness are typically limited to fairly small programs. The reason is the same one that makes proving stronger than testing: the proof must account for every possible run, and the reasoning has to be carried through each statement's effect on state until the final result is shown to match the specification.
This does not make the ideas useless for larger work. Even in everyday programming, the ideas and techniques used in correctness proofs can guide how you reason about code, even when you never write a formal proof. Asking what state a statement starts from, what it changes, and what result the specification demands are useful habits at any scale.
A Correct Program Can Still Be Useless
A correctness proof establishes that the program matches its specification. It does not establish that the specification describes what users need. A proof of correctness is only as trustworthy as the specification it relies on, and a correct program can still be useless if its specification is wrong. The program may be perfectly faithful to its process and states while the specification remains the weak point.
Mistakes That Blur Testing and Proving
Common mistake Treating a finished proof as the end of the job
What it looks likeA team proves that a program matches its specification and stops, without asking whether the specification reflects what users need.
Why it failsThe proof answers whether the program does what it was asked to do, not whether what it was asked to do was the right thing.
Do this insteadKeep the two questions separate and review the specification with the people who will use the program.
Common mistake Assuming passing tests prove correctness for all inputs
What it looks likeA test suite covers ten inputs and all pass, so the author declares the program correct for every input.
Why it failsTesting samples some runs. The untested runs are not covered by the passing results.
Do this insteadDescribe passing tests as evidence about the cases tested, and reason about the untested cases separately.
Common mistake Ignoring what a statement changes in the state
What it looks likeReasoning about x = x + 1 as if x were still its old value afterward, or assuming y changed when the statement only touches x.
Why it failsA statement's meaning is its effect on the state, so the reasoning must follow the state after each statement.
Do this insteadFor each statement, state what it changes and what it leaves alone, then carry the new state forward.
Common mistake Believing that a correct program must be useful
Tracing a Small Program Yourself
Trace the states of this program, one statement at a time: p = 5, then q = p + 2, then p = q * 2. Write down the state after each statement, then state the final value of p and of q.
Hints
- Start with an empty state. Each statement reads from the current state, so use the values you have already recorded.
- After p = 5 the state holds only p. Work out q before you change p again.
- The last statement changes p but not q, so q keeps the value it had before.
What do you think happens?
After p = 5, q = p + 2, p = q * 2 runs, what is the final value of p?
Reveal answer
14
After p = 5 and q = p + 2, q is 7. Then p = q * 2 gives p as 14. The final state holds p as 14 and q as 7.
Key Takeaways on Proof and Testing
- 1A program is correct when it produces the correct result as specified, so correctness is always measured against a specification.
- 2The state is the information relevant at one moment, and the process is the sequence of states during execution. A statement's meaning is its effect on state.
- 3Testing checks some runs, while a proof reasons about every possible run. Full proofs are typically limited to fairly small programs.
- 4A proof is only as trustworthy as its specification, so a correct program can still be useless if the specification is wrong.
At a glance
- A program is correct when it produces the correct result as specified, so correctness is always measured against a specification.
- State is all relevant information at one moment, and process is the sequence of states during execution; each statement's meaning is its effect on state.
- Testing samples some runs, while a proof reasons about every possible run, though full proofs are typically limited to fairly small programs.
- A proof is only as trustworthy as its specification, so a correct program can still be useless if the specification is wrong.