ZetCode

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.

harness.a68
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.

table.a68
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.

suite.a68
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

The Algol 68 Genie project

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

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Algol 68 tutorials.