Testing COBOL Programs

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against ISO/IEC 1989:2023 (edition 3, January 2023; an upcoming 4th edition is in ballot), which does not define any testing facility, so everything here is a practice built on the language’s CALL, RETURN-CODE and file statements; against IBM Enterprise COBOL for z/OS 6.5 (PTFs through September 2026) on z/OS 3.2; and against GnuCOBOL 3.2 (28 July 2023, the latest release), with which every program and script on it was compiled with cobc -x -Wall (and with -std=ibm, as the Makefile does) and run on 2026-10-20. The vendor tools in the second half are described from their public documentation and could not be run here; they are labelled "z/OS; not run here" and worded without naming a release because their current versions could not be confirmed.

COBOL programs are rarely tested the way a Python or Java program is, and the result is that a fine calculation or a master-file update goes to production on the strength of a few runs by hand. It does not have to be so: a COBOL subprogram is a callable unit like any other, and a driver program that calls it with known inputs and compares the outputs is about twenty lines of COBOL. This page builds that harness step by step with the Circulation fine calculation, and then shows what the commercial and open-source tools add. The language features it uses are on Subprograms: CALL and Linkage and Arithmetic and COMPUTE; running the harness in a pipeline is on DevOps and CI/CD for COBOL, and finding out why a test fails is on Debugging and Troubleshooting.

Where the book is outdated: Taulli’s Modern Mainframe Development (2022, chapter 9) treats testing mostly as a step in the delivery pipeline run with the tools of a vendor, and says little about writing the tests themselves. The practice in this page does not depend on any tool, and the tool descriptions use the names and documentation current at the time of writing, not the book’s.

What to test in batch COBOL

A batch program has no screen to try, so its behaviour is entirely in what it reads, what it writes and what it returns. The useful units are these:

Unit Example in the Circulation run

Paragraph or subprogram logic with no I/O

The fine for a number of days late (FINECALC, below): the rate, the cap, the zero and negative cases.

Record transformations

A LOANTRAN record to a LOAN-RECORD group; the validation that rejects a type other than O or R.

File-status paths

What happens when a READ returns status 10 (end of file), 23 (record not found) or 35 (file missing), that is, the error branches that normal data never reaches.

Whole-program behaviour

A small input file run through the whole program and the output compared with a saved "golden" file.

Boundaries and invalid data

Zero, one, the largest legal value, a value one over it; spaces in a numeric field; the first and last record of a file.

The first two are cheap and are what a unit test harness is for. The last three need files and are tested by running the program on a small data set and comparing outputs, which the harness does with diff.

Test data design follows the same advice as elsewhere: a handful of hand-written records that each target a single rule beat a copy of production data, which is large, slow, personal data and not repeatable. The Circulation data (BOOKMAST, MEMBMAST, LOANTRAN) used throughout this section is a small example of such a set, with deliberately invalid lines added for the reject paths.

A runnable harness: the unit under test

The unit under test is a subprogram that computes the overdue fine, 0.35 per day capped at 5.00, and returns zero when the loan is not late. It is a normal `CALL`able program, with no I/O, which is what makes it easy to test.

      * FINECALC - overdue fine: 0.35 per day, capped at 5.00.
       IDENTIFICATION DIVISION.
       PROGRAM-ID. FINECALC.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  WS-RATE            PIC 9V99 VALUE 0.35.
       01  WS-CAP             PIC 9V99 VALUE 5.00.
       LINKAGE SECTION.
       01  LK-DAYS-LATE       PIC S9(4).
       01  LK-FINE            PIC 9(3)V99.
       PROCEDURE DIVISION USING LK-DAYS-LATE LK-FINE.
       MAIN-PARA.
           IF LK-DAYS-LATE <= 0
               MOVE 0 TO LK-FINE
           ELSE
               COMPUTE LK-FINE ROUNDED = LK-DAYS-LATE * WS-RATE
                   ON SIZE ERROR MOVE WS-CAP TO LK-FINE
               END-COMPUTE
               IF LK-FINE > WS-CAP
                   MOVE WS-CAP TO LK-FINE
               END-IF
           END-IF
           GOBACK.

Source: GnuCOBOL 3.2 Programmer’s Guide: CALL; Enterprise COBOL for z/OS 6.5 Language Reference: PERFORM statement (the pattern of PERFORM of a case paragraph is the one documented there)

The test driver

The driver calls the unit once per case, compares the result with the expected value, prints one line per case and ends by setting RETURN-CODE to the number of failures, so that a shell, a Makefile or a JCL COND test can see success or failure.

      * TESTFINE - test driver for FINECALC; exit status = failures.
       IDENTIFICATION DIVISION.
       PROGRAM-ID. TESTFINE.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  WS-DAYS            PIC S9(4).
       01  WS-FINE            PIC 9(3)V99.
       01  WS-EXPECTED        PIC 9(3)V99.
       01  WS-NAME            PIC X(24).
       01  WS-PASSED          PIC 9(3) VALUE 0.
       01  WS-FAILED          PIC 9(3) VALUE 0.
       PROCEDURE DIVISION.
       MAIN-PARA.
           MOVE "NOT LATE" TO WS-NAME
           MOVE 0 TO WS-DAYS
           MOVE 0 TO WS-EXPECTED
           PERFORM RUN-CASE
           MOVE "THREE DAYS" TO WS-NAME
           MOVE 3 TO WS-DAYS
           MOVE 1.05 TO WS-EXPECTED
           PERFORM RUN-CASE
           MOVE "CAP REACHED" TO WS-NAME
           MOVE 20 TO WS-DAYS
           MOVE 5.00 TO WS-EXPECTED
           PERFORM RUN-CASE
           MOVE "EARLY RETURN" TO WS-NAME
           MOVE -2 TO WS-DAYS
           MOVE 0 TO WS-EXPECTED
           PERFORM RUN-CASE
           MOVE "ONE DAY" TO WS-NAME
           MOVE 1 TO WS-DAYS
           MOVE 0.35 TO WS-EXPECTED
           PERFORM RUN-CASE
           DISPLAY "PASSED " WS-PASSED " FAILED " WS-FAILED
           MOVE WS-FAILED TO RETURN-CODE
           STOP RUN.
       RUN-CASE.
           CALL "FINECALC" USING WS-DAYS WS-FINE
           IF WS-FINE = WS-EXPECTED
               ADD 1 TO WS-PASSED
               DISPLAY "PASS " WS-NAME
           ELSE
               ADD 1 TO WS-FAILED
               DISPLAY "FAIL " WS-NAME " EXPECTED " WS-EXPECTED
                   " GOT " WS-FINE
           END-IF.

The two programs and the next two files form the harness. The RUN-CASE paragraph is the whole "framework": everything else is data.

The Makefile and the golden file

The Makefile compiles the driver and the unit together into one executable, runs it, writes the output to actual.txt and compares it with expected.txt. The exit status of the driver is kept in status so that make fails when there are failures even though diff succeeded.

# Test harness for the Circulation fine calculation (GnuCOBOL 3.2).
COBC   = cobc
FLAGS  = -Wall -std=ibm

.PHONY: test clean

test: testfine
	./testfine > actual.txt; status=$$?; \
	diff -u expected.txt actual.txt && echo "golden output matches"; \
	exit $$status

# The unit under test is linked into the driver.
testfine: TESTFINE.cob FINECALC.cob
	$(COBC) -x $(FLAGS) -o $@ TESTFINE.cob FINECALC.cob

clean:
	rm -f testfine actual.txt

The golden file expected.txt is the output the driver should print, which is itself the specification of the unit in executable form:

PASS NOT LATE
PASS THREE DAYS
PASS CAP REACHED
PASS EARLY RETURN
PASS ONE DAY
PASSED 005 FAILED 000

Run with make test in GnuCOBOL 3.2 (the commands are cobc -x -Wall -std=ibm -o testfine TESTFINE.cob FINECALC.cob and then the driver), the build produces no warnings and the run prints:

cobc -x -Wall -std=ibm -o testfine TESTFINE.cob FINECALC.cob
./testfine > actual.txt; status=$?; \
diff -u expected.txt actual.txt && echo "golden output matches"; \
exit $status
golden output matches

A change that breaks the rule is caught immediately. When the rate in FINECALC was edited from 0.35 to 0.30, the same make test produced this report and the exit status 2 (the number of failed cases):

--- expected.txt
+++ actual.txt
@@ -1,6 +1,6 @@
 PASS NOT LATE
-PASS THREE DAYS
+FAIL THREE DAYS               EXPECTED 00105 GOT 00090
 PASS CAP REACHED
 PASS EARLY RETURN
-PASS ONE DAY
-PASSED 005 FAILED 000
+FAIL ONE DAY                  EXPECTED 00035 GOT 00030
+PASSED 003 FAILED 002
make: *** [Makefile:8: test] Error 2

(The diff header lines also carry the file time stamps, which are left out above.) The values print as 00105 because -std=ibm does not show the implied decimal point; with the default dialect the same run prints 001.05. Without make, the same harness is two commands in a shell script:

#!/bin/sh
cobc -x -Wall -std=ibm -o testfine TESTFINE.cob FINECALC.cob || exit 1
./testfine > actual.txt
status=$?
diff -u expected.txt actual.txt || status=1
exit $status

Growing the harness

  • More cases. Add a MOVE/PERFORM RUN-CASE group per case, or read cases from a small sequential file of days and expected fines so that a tester can add a case without touching the program.

  • File-status paths. A driver can create a small file, open it, force the error (open a file that is not there, read past the end) and assert on the FILE STATUS value. The values to expect are on File Status Codes.

  • Whole programs. Run the real program with COB_* file-mapping environment variables or ASSIGN names pointing at test files, then diff the output file with a golden one, which suits report programs and the output of Sort and Merge.

  • Checking the harness itself. A test that has never failed has not been shown to work: edit the unit as above and see it fail once.

Doubles: mocking files, CALLs, SQL and CICS

A unit that calls another program, reads a file, issues SQL or CICS commands cannot be run on a laptop as it is. The ways to deal with it are the same in every language: replace the dependency with a test double.

  • CALL. A called program is found by name, so a stub program with the same PROGRAM-ID that returns canned values is simply linked in place of the real one. The Makefile above does exactly that for FINECALC and can link a stub instead of any other called program.

  • Files. The ASSIGN name decides which file is opened, so a test points it at a small test file (GnuCOBOL environment variables, a JCL DD on z/OS).

  • SQL and CICS. Embedded EXEC SQL and EXEC CICS statements are translated into calls to the Db2 and CICS stubs before compilation, so a laptop build needs those stubs replaced; GnuCOBOL does not translate them without an additional pre-compiler (see Embedded SQL and Db2). The z/OS tools below automate the replacement: they record or define the results of the calls so that the test can run in a batch region without the subsystem.

z/OS tools

Everything in this section is described from the public documentation of each product and is not run here.

IBM zUnit (IBM Z Open Unit Test). IBM’s unit-test tool for COBOL and PL/I on z/OS. The documentation describes test cases that are generated for a program or recorded from a run of it, a test program that calls the unit with the input of the case and checks the outputs, and stubs for the external calls the unit makes so that it can run on its own in batch; the details of the result format are left to the documentation. The zUnit documentation is at IBM Z Open Unit Test. IBM Developer for z/OS and its related test-automation offerings integrate it; their exact packaging and release names are not stated here because they could not be confirmed.

Broadcom Test4z. Broadcom’s unit-test tool in the Code4z family, aimed at running COBOL tests from VS Code. Broadcom’s documentation is the authority for its current commands and licence conditions, and the location of that documentation could not be confirmed during this verification, so no link is given.

Galasa. An open-source framework (Open Mainframe Project) for integration testing of applications on z/OS in a hybrid cloud. A single Galasa test can drive 3270 terminals, batch jobs and other interfaces, so it is the tool for tests that cover a CICS transaction such as the Circulation CINQ inquiry through its screens. Documentation is at galasa.dev.

COBOL Check. An open-source Open Mainframe Project framework in which tests were written in a COBOL-like language, merged into the program source and run as a normal COBOL program with mocks for files, CALL, SQL and CICS. The repository was archived by its owner on 28 May 2026 and is read-only, and its README calls the project outdated, so it should not be chosen for new work; it is listed so that a reader who meets an existing suite knows its status. github.com/openmainframeproject/cobol-check

Which to choose: the Makefile harness for logic that needs only GnuCOBOL; zUnit or Test4z for tests that must run in the same z/OS environment as production, with mocks of the subsystems; Galasa for end-to-end tests across several interfaces.

Common mistakes

  • Testing only through the whole program. A failing nightly run says that something broke, not what; a unit test says which rule.

  • A test with no failing run. If the driver always prints PASS, change the unit once and watch it fail.

  • Comparing the wrong dialect’s output. A golden file made with the default dialect (001.05) does not match a -std=ibm run (00105); build and compare with the same flags.

  • Forgetting RETURN-CODE. Without it the driver always exits 0 and a pipeline cannot tell failure from success.

  • Expected values computed by the same formula. Put the expected result in the test as a literal worked out by hand.