10.1 Program Correctness: Preconditions, Postconditions, Invariants, and Testing vs. Proof

Key Takeaways

  • A precondition states what must be true before a procedure is called, such as "n is a positive integer" or "the array is sorted"; the caller is responsible for meeting it.
  • A postcondition states what a procedure guarantees when it finishes, provided its preconditions held, such as "returns the largest value in the array."
  • A loop invariant is a condition that is true before and after every iteration; in a find-the-maximum loop, max is always the largest value examined so far.
  • Testing (empirical evidence) can reveal bugs but cannot prove their absence, because it checks only some inputs; a proof establishes correctness for all valid inputs.
  • To compare two code segments, check whether they produce the same result for every valid input, especially boundary cases, and then compare efficiency and clarity.
Last updated: September 2026

What this competency asks

ETS asks you to understand how to analyze computer programs in terms of correctness:

  1. Trace code and indicate the output printed or the value of variables after execution.
  2. Indicate the inputs that produce given outputs for a code segment.
  3. Describe what a program does, or choose the code segment that correctly implements an intended purpose.
  4. Identify valid preconditions and postconditions.
  5. Compare two code segments or algorithms.
  6. Identify the type of error produced, and errors and fixes (Section 10.2).

The debugging competency adds: differentiate between empirical testing and proof.

What "correct" means

A program is correct if it meets its specification for every valid input. Passing one example does not make it correct. The specification is often written as a precondition and a postcondition.

Preconditions and postconditions

// precondition: n ≥ 1 and a[0] … a[n - 1] contain integers
// postcondition: returns the largest value among a[0] … a[n - 1]
int findMax ( int[ ] a, int n )
    int max ← a[0]
    for ( int i ← 1; i < n; i ← i + 1 )
        if ( a[i] > max )
            max ← a[i]
        end if
    end for
    return max
end findMax
TermMeaningIn findMax
PreconditionMust be true before the call; the caller's responsibilityn ≥ 1, because a[0] must exist
PostconditionGuaranteed after the call, if the precondition heldThe return value is the largest element

ETS's sample procedures state preconditions in comments, such as // precondition: n is a positive integer.

Identifying valid preconditions

Ask: what could make this code fail or give a meaningless answer? The precondition should rule out exactly those inputs.

ProcedureValid preconditionWhy
Binary searchThe array is sorted in the order the code assumesOtherwise halving can discard the target
average ( a, n )n > 0Division by n
findMax ( a, n )n ≥ 1Reads a[0]
sqrtApprox ( x )x ≥ 0No real square root for negative numbers
substring ( b, e )0 ≤ b ≤ e ≤ lengthPositions must exist

A precondition that is too strong is also a poor choice. "n = 10" for findMax is true of some valid calls, but the code works for any n ≥ 1.

Identifying valid postconditions

A postcondition describes the result or final state, not the steps taken:

  • Good: "returns the index of the first occurrence of target, or −1 if target is not in the array."
  • Good: "the elements of arr are in nondecreasing order and are a rearrangement of the original elements."
  • Poor: "loops through the array." That describes steps, not a guarantee.

For a sort, "the array is sorted" is not enough on its own. A procedure that overwrote everything with zeros would satisfy it. The postcondition must also say the array holds the same values.

Loop invariants

A loop invariant is a condition that is true before the loop starts and remains true after every iteration. When the loop ends, the invariant plus the exit condition gives the postcondition.

For findMax, the invariant at the top of each iteration is: max is the largest value in a[0] … a[i − 1].

  • Initially (i = 1): max = a[0], the largest of a[0] … a[0]. True.
  • Maintained: if a[i] > max, max becomes a[i]; otherwise max stays. Either way max is the largest of a[0] … a[i].
  • At exit (i = n): max is the largest of a[0] … a[n − 1], which is the postcondition.

ETS's own explanation of its find-the-maximum sample uses exactly this reasoning. An invariant is also a quick way to judge a missing condition: the choice that keeps the invariant true is the correct one.

Finding inputs that produce a given output

Work backward from the output, or test each candidate input:

int mystery ( int x )
    if ( x % 3 == 0 )
        return x / 3
    else
        return x + 1
    end if
end mystery

Which inputs make mystery return 4? Working backward: if x is a multiple of 3, then x / 3 = 4 gives x = 12. Otherwise x + 1 = 4 gives x = 3, but 3 is a multiple of 3, so that case never uses x + 1 (mystery(3) returns 1). The only input is 12. Checking the "else" case for consistency is exactly the step that separates the right answer from a tempting wrong one.

Comparing two code segments or algorithms

When asked whether two segments are equivalent, or which is better:

  1. Same results? Test ordinary values and boundary values: 0, 1, negative numbers, empty arrays, equal elements, and the first and last positions. One disagreement proves they are not equivalent.
  2. Same side effects? Do both modify the array, print the same output, or change the same variables?
  3. Efficiency: compare growth rates (Section 5.1). For example, both segments may be correct, but one is O(n²) and the other O(n).
  4. Clarity and maintainability: fewer duplicated statements, meaningful names, and no hard-coded values (Section 10.4).

Empirical testing vs. proof

Empirical testingProof
MethodRun the program on selected inputs and compare with expected outputsLogical argument (for example, loop invariants or induction) that the code meets its specification
CoversOnly the inputs triedAll inputs that satisfy the precondition
Can showThat a bug exists (a failing test)That no bug exists relative to the specification
CostCheap, automatable, repeatableDemanding; used for critical code

Edsger Dijkstra put the limitation of testing this way: testing can show the presence of bugs, but never their absence. A function that returns the correct result for 50 test inputs can still fail on the 51st, for example on an empty array or a negative number that no test included. In practice, teams combine thorough testing (Section 10.3) with reasoning such as invariants, and reserve formal proof for the most critical software.

Test Your Knowledge

Which is the most appropriate precondition for a binary search procedure that returns the index of target in array a?

A
B
C
D
Test Your Knowledge

A procedure should sort an integer array arr in increasing order. Which is the best postcondition?

A
B
C
D
Test Your Knowledge

A student's procedure passes 200 automated test cases. The student concludes that the procedure is correct for all inputs. What is the best response?

A
B
C
D
Test Your Knowledge

Given this procedure, which input returns 4?

int mystery ( int x )
    if ( x % 3 == 0 )
        return x / 3
    else
        return x + 1
    end if
end mystery

A
B
C
D