Unit testing in Algol 68
last modified November 14, 2026
Algol 68 Genie ships with the language and the standard prelude, not with a test framework. That is no obstacle: a harness that counts checks, prints the failures, and ends with a non-zero status fits in about thirty lines, and the programs in this series are short enough that a single file of tests is all they need.
This article builds that harness, uses it to test a recursive procedure, reads the report of a failing check, drives the same checks from a table of cases and from a row of test procedures, and explains what the harness cannot do.
Why a harness at all
The alternative to a harness is a program that prints values and a reader who compares them with expected values by eye. That works once. A harness turns each comparison into a check that either passes silently or reports itself, and it ends with one number and one exit status that a script can test.
The exit status is the important part. Algol 68 has no way to return a chosen
status from a normal run, and stop always reports success, but a
false assertion reports failure. The harness therefore finishes with
ASSERT (failed = 0), which makes the process return 1 when
anything failed.
The harness
Three variables hold the state: the number of checks that passed, the number
that failed, and the name of the test that is running. start
records the name, check compares a condition with what it should
be, and summary prints the totals and asserts that nothing
failed.
INT passed := 0;
INT failed := 0;
STRING current := "";
PROC start = (STRING name) VOID:
(
current := name;
print(("test ", name, newline))
);
PROC check = (BOOL condition, STRING what) VOID:
(
IF condition THEN
passed +:= 1
ELSE
failed +:= 1;
print(("FAIL ", current, ": ", what, newline))
FI
);
PROC summary = VOID:
(
print((whole(passed, 0), " passed, ", whole(failed, 0), " failed", newline));
ASSERT (failed = 0)
);
PROC fact = (INT n) INT:
IF n <= 1 THEN 1 ELSE n * fact(n - 1) FI;
BEGIN
start("factorial");
check(fact(0) = 1, "0! is 1");
check(fact(5) = 120, "5! is 120");
check(fact(10) = 3628800, "10! is 3628800");
summary
END
A passing check prints nothing, so the transcript of a healthy run is one line per test and one summary line. The expectation is a string supplied by the caller, so the harness itself needs no formatting: the comparison is a boolean, and the message is printed only when it is false.
$ a68g harness.a68 test factorial 3 passed, 0 failed $ echo $? 0
Reading a failure
A wrong expectation changes one line of the same file, which is saved as
fail.a68: the second check compares fact(5) with 121
instead of 120. The check fails, the
counter records it, and the summary reports two passes and one failure before
the assertion stops the program.
check(fact(5) = 121, "5! is 121");
The FAIL line names the test and the expectation, which is
usually enough to find the mistake. The three lines after the summary are the
report of the false assertion: the source line, a marker under the condition,
and the message. The process returns 1, so a shell script or a continuous
integration job sees the failure.
$ a68g fail.a68
test factorial
FAIL factorial: 5! is 121
2 passed, 1 failed
24 ASSERT (failed = 0)
1
a68g: runtime error: 1: false assertion (detected in VOID closed-clause starting at "(" in line 22).
$ echo $?
1
Table-driven tests
A procedure with several interesting inputs is easier to test from a table
than from a series of calls. The table is a row of a mode that holds the input
and the expected result, and the loop compares the procedure's answer with
each row. CASE is a keyword and cannot name the mode, so the mode
is called EXAMPLE.
MODE EXAMPLE = STRUCT (INT input, INT expected);
INT passed := 0;
INT failed := 0;
PROC check = (BOOL condition, STRING what) VOID:
(
IF condition THEN
passed +:= 1
ELSE
failed +:= 1;
print(("FAIL: ", what, newline))
FI
);
PROC fact = (INT n) INT:
IF n <= 1 THEN 1 ELSE n * fact(n - 1) FI;
BEGIN
[]EXAMPLE cases = ((0, 1), (1, 1), (5, 120), (10, 3628800));
FOR i TO UPB cases DO
check(fact(input OF cases[i]) = expected OF cases[i],
whole(input OF cases[i], 0) + "! is " +
whole(expected OF cases[i], 0))
OD;
print((whole(passed, 0), " passed, ", whole(failed, 0), " failed", newline))
END
Adding a case is one more pair in the row and nothing else, which is the
reason to prefer the table once the number of inputs passes three or four.
The message is built with +, because it has to name the input
and the expected value that the row supplied.
$ a68g table.a68 4 passed, 0 failed
A row of tests
Separate procedures keep independent tests apart. A procedure of mode
PROC VOID is a value, so the tests can be stored in a row and
called in a loop, and adding a test means adding one entry to the row.
INT passed := 0;
INT failed := 0;
STRING current := "";
PROC start = (STRING name) VOID:
(
current := name;
print(("test ", name, newline))
);
PROC check = (BOOL condition, STRING what) VOID:
(
IF condition THEN
passed +:= 1
ELSE
failed +:= 1;
print(("FAIL ", current, ": ", what, newline))
FI
);
PROC test sum = VOID:
(
start("sum");
check(1 + 2 = 3, "1 + 2");
check(0 + 0 = 0, "0 + 0")
);
PROC test strings = VOID:
(
start("strings");
STRING s = "abc";
check(UPB s - LWB s + 1 = 3, "length of abc");
check(s = "abc", "abc equals abc")
);
BEGIN
[]PROC VOID tests = (test sum, test strings);
FOR i TO UPB tests DO
tests[i]
OD;
print((whole(passed, 0), " passed, ", whole(failed, 0), " failed", newline))
END
The row is declared with the mode PROC VOID, and
tests[i] calls the procedure that the element refers to. The
tests share the counters and the current name, which is what makes the report
work; they are not isolated from each other, so a test that changes a global
can affect the next one.
$ a68g suite.a68 test sum test strings 4 passed, 0 failed
What the harness does not do
The harness runs every check even after one fails, but it cannot continue after a runtime error: an out-of-bounds subscript or a division by zero ends the program, and the checks after it never run. A test that may raise such an error belongs in its own file, or behind a condition that prevents the error.
The final assertion is also the weak point of the design, because
--noassertions removes it. With that option the summary still
prints 1 failed but the process returns 0, and a script that
trusts the status will call the run successful.
The error handling article explains assertions and exit statuses in more detail, and the compilation article shows how to run a whole directory of such files from a script.
$ a68g --noassertions fail.a68 test factorial FAIL factorial: 5! is 121 2 passed, 1 failed $ echo $? 0
Source
Revised Report on the Algorithmic Language Algol 68
In this article we wrote a test harness in thirty lines, tested a recursive procedure, read the report of a failing check, drove the checks from a table of cases and from a row of procedures, and noted where the harness stops.
Author
List all Algol 68 tutorials.