BankLang

zUnit integration

bankc zunit <project> writes the three artifacts IBM's z/OS Automated Unit Testing Framework needs to run a generated program on a mainframe and report pass or fail.

pnpm bankc zunit examples/zunit-tested-posting
Wrote dist/zunit/TZUNITTE.bzucfg
Wrote dist/zunit/TZUNITTE.cbl
Wrote dist/zunit/TZUNITTE.jcl

This page is longer than the feature, on purpose. Every shape in those files is copied from test cases IBM's own generator produced, and each one is cited below — because the failure mode here is not a compiler error, it is a file somebody uploads to a mainframe and submits.


Where the shapes come from

IBM's zUnit documentation is not in vendor-docs/, and ibm.com/docs refuses automated retrieval. What this generator was built against instead is test cases IBM's editor produced, published in public repositories:

Source What it settled
retirementCalculator/testcase/EBUD01.bzucfg The configuration at 4.0.0.0: root element, namespace, element order
retirementCalculator/testcase/TEBUD01.cbl The whole driver: BZUGETEP, BZUASSRT, BZU_INIT, stubs, GTMEMRC
retirementCalculator/jcls/RUNTAZ.jcl EXEC PROC=EQAPPLAY, its DDs, and PRM='STOP=E,REPORT=XML'
myapp/.../DATSUB.bzucfg and TDATSUB.cbl A case for a program with a parameter: supplied values and compares
SampleMortgage/cobol/hello.bzucfg A case at 3.0.0.0, and one with no playback file at all
genapp-demo/tests/LGICDB01.bzucfg type="CICS", and TLGICDB01 truncated to the member name TLGICDB0
retirementCalculator/application-conf/Cobol.properties cobol_compileDebugParms=TEST — the program under test is compiled with TEST

Two values in the generated configuration are inferred rather than observed. They are named as such in divergences.md, D20 and D21, with the fallback written down for each.


Writing a test

A test is a declaration in the same file as the program, in the same language:

test postsBothLegs for postOne {
  given account = "0001234567890123";
  given amount = 100.00;
  given idempotencyKey = "IDEM-0001";
  expect debit("0001234567890123", 100.00);
  expect credit("SUSPENSE", 100.00);
  expect audit("POSTED", "IDEM-0001");
}

It compiles to nothing. tests/zunit.test.ts asserts that a program's COBOL is byte for byte what it is with the tests removed, because an artifact that ships must not depend on the tests written against it.

Why the surface is this narrow

The driver is a separate program. The runner enters the program under test through its entry point and intercepts the modules it calls; it does not share its storage. So what a case can observe is:

and that is exactly what given and expect are. A test that appeared to assert on the program's WORKING-STORAGE would be reporting a pass nobody checked, which is worse than having no test.

given

One scalar parameter of the entry transaction, which is one field of the PARM. A record parameter is refused (BANK-TEST-003): it is a buffer the program fills from a file, so there is nothing for a caller to supply.

Values are literals and only literals (BANK-TEST-004). The generated driver holds them in MOVE statements and evaluates nothing — a test that computed its expected value would be a second implementation of the program, running on the mainframe, with nothing checking it.

expect

The calls to BANKLEDG and BANKAUDT, in order. The generated stub counts each call and compares it against the expectation for that position, so a debit then a credit is not a credit then a debit. Both directions of miscount fail:

What a test cannot say yet


The three artifacts

The configuration

<?xml version="1.0" encoding="UTF-8"?>
<runner:RunnerConfiguration xmlns:runner="http://www.ibm.com/zUnit/4.0.0.0/TestRunner" id="d012252c-…">
  <runner:options contOnTestCaseError="false" … fileIOCapture="compat"/>
  <runner:testCase moduleName="TZUNITTE">
    <test name="POSTSBOTHLEGS" entry="TEST_POSTSBOTHLEGS" type="BTCH"
          init="BZU_INIT" term="BZU_TERM" program="ZUNITTES" … noPlaybackData="true"/>
  </runner:testCase>
  <runner:intercept module="ZUNITTES" stub="false" lengths="73" parmtype="I" retcode="true" exist="false"/>
  <runner:intercept module="BANKLEDG" stub="true" lengths="48" parmtype="I" retcode="false" exist="false"/>
  <runner:intercept module="BANKAUDT" stub="true" lengths="96" parmtype="I" retcode="false" exist="false"/>
  <runner:playback moduleName="ZUNITTES"/>
  <runner:fileAttributes hlqDdName="AZUHLQ"/>
</runner:RunnerConfiguration>

The driver

One compilation unit holding several sibling programs, which is the shape IBM's generator produces:

Program What it does
TEST_<NAME> One per test: zeroes the counters, builds the PARM, enters the program
BZU_TEST The runner's callback around the program under test
BZU_INIT/BZU_TERM Run before and after each test; BZU_INIT answers with the case's id
PGM_BANKLEDG, PGM_BANKAUDT The stubs the calls arrive at, and where they are checked
GTMEMRC Hands out one call counter per stubbed module

It opens with the compiler options IBM's generator uses, and each one earns its place:

       PROCESS NODLL,NODYNAM,TEST(NOSEP),NOCICS,NOSQL,PGMN(LU),NOSEQ

TEST is what puts the hooks in that calls are intercepted through — the mechanism is the z/OS Debugger's, which is why the info block comes from a copybook named EQAITERC — and PGMN(LU) is what lets TEST_POSTSBOTHLEGS be a program-name at all. The program under test needs TEST as well; the generated JCL says so, and a program compiled without it runs and calls the real BANKLEDG.

Entering the program is IBM's sequence, copied:

           CALL BZUGETEP USING BY REFERENCE PROGRAM-NAME AZ-CSECT
               RETURNING AZ-EP-PTR
           SET ADDRESS OF AZ-PROC-PTR TO AZ-EP-PTR
           CALL AZ-PROC-PTR USING BANK-PARM

and so is reporting a failure:

           CALL BZUASSRT USING BZ-P1 BZ-P2 BZ-P3 BZ-ASSERT

where BZ-P1 is 4, BZ-P2 is 2001 and BZ-P3 is 'AZU'.

One thing this generator does not copy: IBM's editor renames every data item to ZUT00000001 and puts the real name in a comment beside it. That is an artifact of its model keying items by identifier, and nothing in the runner reads a data name — the failure message carries the name as a literal — so the generated driver keeps the names the program uses.

The job

//COMPILE  EXEC IGYWCL
//COBOL.SYSIN DD DISP=SHR,DSN=BANKLANG.ZUNIT.COBOL(TZUNITTE)
//LKED.SYSLMOD DD DISP=SHR,DSN=BANKLANG.TEST.LOADLIB(TZUNITTE)
//RUNNER   EXEC PROC=EQAPPLAY,COND=(4,LT),
//         BZUCFG=BANKLANG.ZUNIT.BZUCFG(TZUNITTE),
//         BZUCBK=BANKLANG.TEST.LOADLIB,
//         BZULOD=BANKLANG.TEST.LOADLIB,
//         PRM='STOP=E,REPORT=XML'

EQAPPLAY is the procedure a working pipeline submits the runner through. Older documentation names BZUPPLAY; the observed job and the zAppBuild properties that drive it both use EQAPPLAY, and the parameter string is theirs.

No PARM is passed to the compile step: the options are in the driver's PROCESS statement, which is where Enterprise COBOL reads them from and which overrides what a procedure passes.


What this has been through, and what it has not

It compiles. tests/zunit.test.ts runs cobc -fsyntax-only over a generated driver under GnuCOBOL's default dialect and under tools/banklang-ibm.conf, and both accept it — nested sibling programs, ENTRY statements, procedure pointers and all.

That evidence is narrower than it sounds. COPY EQAITERC resolves locally to runtime/zunit/EQAITERC.cpy, a stand-in declaring the two fields the driver names — because IBM's own copybook is not here. What the compile establishes is that the syntax is accepted and every name resolves. It establishes nothing about the info block's layout.

No generated case has been run. Not locally — there is no runner here — and not on z/OS, because zos/README.md has no RESULTS.md yet. This is the "compiled" grade in evidence/GRADES.md and not the "executed" one, and the two inferred values in D20 and D21 are the kind of thing a single real run would settle.


Read this page as Markdown on GitHub →