26 Testing Your Programs
Contracts improve correctness, but they do not remove the need for tests.
A contract states what must always be true. A test chooses specific examples and checks that the program behaves correctly on them. Good software uses both.
26.1 What Tests Add
A contract can tell us:
- the balance decreases by
amount - the result is non-negative
- the stack is not empty before
pop
But a test can still reveal problems that contracts may not express directly or completely:
- a boundary case the programmer forgot
- a wrong algorithm that satisfies a weak postcondition
- an interaction between several routines
- a regression after later changes
Tests are the concrete examples that keep abstractions honest.
26.2 Test Small Things First
The best starting point is the smallest meaningful unit.
For a function:
nex> function square(x: Integer): Integer
do
result := x * x
end
simple tests might be:
nex> square(0)
0
nex> square(5)
25
nex> square(-3)
9
The point is not the print statements themselves. The point is to choose cases that exercise the routine’s meaning:
- a neutral case
- a typical case
- an edge or surprising case
26.3 A Tiny Test Harness in Nex
Nex does not need a large testing framework before you can begin writing useful tests. A simple helper routine already goes a long way:
nex> function assert_equal_integer(actual, expected: Integer, label: String)
do
if actual /= expected then
raise "test failed: " + label
end
end
Then:
nex> function test_square()
do
assert_equal_integer(square(0), 0, "square zero")
assert_equal_integer(square(5), 25, "square positive")
assert_equal_integer(square(-3), 9, "square negative")
print("square tests passed")
end
This is not elaborate, but it is enough to establish the core habit: expected behavior should be checked automatically, not only by eye.
26.4 Testing Contracts
Contracts and tests reinforce each other.
Tests should include valid cases that satisfy the precondition and confirm the promised behavior.
They should also include deliberate invalid calls when appropriate, to confirm that contract violations occur where expected.
For a stack:
- create a new stack
- push one element, then pop it
- push several elements and check last-in, first-out behavior
- deliberately call
popon an empty stack and confirm the precondition fails
The valid tests check behavior. The invalid test checks the interface boundary.
26.5 Testing Classes Through Sequences
Class tests are often more meaningful as sequences of operations than as isolated calls.
For Account:
- create with initial balance
100.0 - deposit
25.0 - withdraw
40.0 - check that the balance is
85.0
In Nex:
nex> function test_account()
do
let a := create Account.make(100.0)
a.deposit(25.0)
a.withdraw(40.0)
if a.balance /= 85.0 then
raise "test failed: account sequence"
end
print("account tests passed")
end
The sequence matters because the behavior of later operations depends on earlier ones.
26.6 Choosing Good Test Cases
Choose tests that represent different categories of behavior:
Normal cases.
The routine works under ordinary expected inputs.
Boundary cases.
Empty arrays, one-element arrays, zero, maximum allowed value, minimum allowed value.
Error cases.
Inputs that should violate a contract or trigger a controlled failure.
Representative combinations.
For classes, sequences of operations that exercise state changes.
One of the most common beginner mistakes is to test only happy-path examples. Real confidence comes from boundary and failure cases.
26.7 Organizing Tests
As programs grow, tests should be kept separate from the main code where possible.
One reasonable structure is:
- source files under
src/or application directories - Nex examples and tutorial code in their own files
- host-side or repository-level automated tests under
test/
This repository already includes automated test commands for the implementation itself:
clojure -M:test test/scripts/run_tests.cljFor your own Nex tutorial programs, a similar habit is useful: create test routines, group them clearly, and run them together.
26.8 A Worked Example: Testing a Frequency Counter
Return to the word-frequency routine:
nex> function word_frequencies(text: String): Map[String, Integer]
do
result := {}
let words := text.to_lower.split(" ")
across words as w do
let count := result.try_get(w, 0)
result.put(w, count + 1)
end
end
A useful test routine:
nex> function test_word_frequencies()
do
let freq := word_frequencies("to be or not to be")
if freq.get("to") /= 2 then
raise "test failed: count of to"
end
if freq.get("be") /= 2 then
raise "test failed: count of be"
end
if freq.get("or") /= 1 then
raise "test failed: count of or"
end
print("word frequency tests passed")
end
This test checks several representative counts from one input. Better still would be to add:
- an all-unique case
- a case-insensitive case
- perhaps a case with repeated spaces, depending on the intended splitting behavior
Tests grow naturally by exploring the routine’s meaning.
26.9 Summary
- Contracts and tests serve different purposes and are both necessary
- Tests should cover normal, boundary, and failure cases
- A small handmade assertion routine is enough to begin
- Class tests are often best written as sequences of operations
- Weak specifications should be reinforced with targeted tests
- Good test organization keeps checking repeatable and easy to run
26.10 Exercises
1. Write a tiny assertion routine for strings and use it to test a reverse(s: String): String function.
2. Write tests for the Stack[G] class that cover push, pop, peek, and the empty-stack precondition.
3. Design a test sequence for Bank_Account that checks deposit, withdrawal, and one invalid call.
4. Improve test_word_frequencies with at least two additional cases that probe boundary or formatting behavior.
5.* Pick one routine whose contract is weaker than its true intent. Write a set of tests that would catch an incorrect implementation even if the current postcondition would not.