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 ( |
Record transformations |
A |
File-status paths |
What happens when a |
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-CASEgroup 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 STATUSvalue. The values to expect are on File Status Codes. -
Whole programs. Run the real program with
COB_*file-mapping environment variables orASSIGNnames pointing at test files, thendiffthe 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 samePROGRAM-IDthat returns canned values is simply linked in place of the real one. TheMakefileabove does exactly that forFINECALCand can link a stub instead of any other called program. -
Files. The
ASSIGNname decides which file is opened, so a test points it at a small test file (GnuCOBOL environment variables, a JCLDDon z/OS). -
SQL and CICS. Embedded
EXEC SQLandEXEC CICSstatements 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=ibmrun (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.