Concepts / Testing Versus Proving Program Behavior

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.

  • Programming

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?

  • Yes, passing all tests proves it is correct

  • No, passing tests does not settle whether the program meets the real need

Reveal answer
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

Definition

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.

Worked example

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.

  1. 1
    State 0 (before any statement)

    No variables exist yet, so the state holds no information.

  2. 2
    After a = 2

    The state now records a as 2. Nothing else has changed.

  3. 3
    After 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.

  4. 4
    After 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.

Result

The process is the sequence of four states, and the final state holds a as 3 and b as 6.

State 0no variablesState 1a is 2State 2a is 2, b is 6State 3a is 3, b is 6a = 2b = a * 3a = a + 1
How does the program's state change step by step as each statement executes, and where does the process end?

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.

python
Output (expected)
State after the last line: x is 4, y is 7
Notes on the code
  • 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.
x3x4y7y7
What changes in the program's state after the assignment x = x + 1 runs, and what stays the same?

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.

QuestionTestingProving
What is examinedSome chosen runs of the programThe program's behavior on every possible run
What it establishesCorrect behavior on the cases testedFinal result matches the specification for every run, if the proof holds
Main limitSilent about inputs nobody triedHard to carry out, and only as trustworthy as the specification
Typical scaleCan be applied to many kinds of programsFull 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 like

    A team proves that a program matches its specification and stops, without asking whether the specification reflects what users need.

    Why it fails

    The 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 instead

    Keep 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 like

    A test suite covers ten inputs and all pass, so the author declares the program correct for every input.

    Why it fails

    Testing samples some runs. The untested runs are not covered by the passing results.

    Do this instead

    Describe 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 like

    Reasoning 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 fails

    A statement's meaning is its effect on the state, so the reasoning must follow the state after each statement.

    Do this instead

    For 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

EASY

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?

  • 7

  • 14

  • 10

Reveal answer
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

  1. 1
    A program is correct when it produces the correct result as specified, so correctness is always measured against a specification.
  2. 2
    The 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.
  3. 3
    Testing checks some runs, while a proof reasons about every possible run. Full proofs are typically limited to fairly small programs.
  4. 4
    A 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.