Integrating the z/OS Automated Unit Testing Framework (zUnit) with EWM
This article applies to Unit testing for the mainframe with zUnit that is supplied with IDz post v14.2.3 or later. IDz supports the creation of zUnit, similar in concept to junits with Java, but applied to mainframe programs. In this article we will focus on zUnit targeting a CICS runtime.
In the same way that junit testcases live with the code they are testing; it makes sense for the zUnit testcases to be stored with the programs they are testing. A zUnit Test Case will comprise of a config file, a test program and a zUnit playback file to drive the process, although the zUnit playback file is not always required. There is also the json file containing the contents of the test data editor. This is a local file called the test case generation configuration file or test case data file.
This article will describe the end-to-end process, starting with the capture of the Test Case in IDz, assigning the relevant EWM definitions, and running an EWM build to compile the programs and run the Test Case.
Capturing the Test Case
Let’s take a look at the steps required to capture the Test Case in IDz. The example we are capturing below is for a typical COBOL CICS zUnit test. Batch will be slightly different as it can have more than 1 playback file, and may have additional DDs required to run based on the files required in the batch program.
Setup and preferences
The first thing to look at are the preferences in IDz. Make sure that “Prompt to change the test case generation configuration file” is checked. This option allows us to put the test case configuration file (.json file for local test cases) in a different folder to the source code (.cbl file). We will see this later in the article when we start recording a test case. Also that the Dynamic test runner (BZUPLAY) is selected, as this is the most recent zUnit runner, rather than the legacy AZUTSTRN version. The prefix letter will prefix the Test Case program with the selected letter and generate the name from the program being tested. This name will be set in the config file and will be used in the EWM build process to point to the test case program when the config file is parsed.
The next thing to do is configure where the playback file will be generated on z/OS. When the Test Case is generated, this file will then be copied into the local project in your workspace in binary.
Project creation
The first thing to do is create a local z/OS Project that will hold the program being tested and the zUnit artifacts required to do the testing. As the project is going to be shared with EWM it should be an EWM z Component Project, with a zOSsrc directory which contains zFolders to hold all the required parts. Our example program is a COBOL program so we have COBOL and COPYBOOK zFolders. For the zUnit artifacts we are going to need zFolders for the config file, playback file and the test case (a program generated to run the test). So our example creates BZUCFG, BZUPLAY and TESTPGM zFolders.
Next we need to associate the project with a property group such that zUnit knows what type of program is going to be tested. Right click on the project, then in the context menu select Property Group –> Associate Property Group… Select an existing property group that has CICS and Db2 options selected:
Test Case Creation/Modification
We can now begin to generate the Test Case. Right click on the program being tested, in our case lgicdb01.cbl and select z/OSAutomated Unit testing –> Generate Test Case…:
This will open a dialog box where we can specify the project, folder and name where the test case generation configuration file will be placed:
If you check the Replace box, generation will create a new test case and overwrite the previous one. So, do not select this unless you wish to overwrite the original test case. Clicking OK, we then select the destination folders for the configuration file, playback file and Test Case program. Select the destination folders in the project you created and click Next:
Clicking Next opens the Test Case editor.
We are not going to change anything here, but we are going to start recording, so click on the Record data button on the action bar:
The recording will start when you press the Start Recording button:
As our example is a COBOL/CICS/Db2 program we are going to login to our CICS Region and execute the transaction with the corresponding COBOL programs. Login to CICS and execute the CICS transaction, in our case SSC1:
Then perform some function in the transaction, in our example performing a Customer Inquiry:
We have executed our CICS transaction, so we need to make sure that the data set mapping is correct, especially for the playback file. This file needs to be stored in EBCDIC and in binary such that non-roundtripable characters are not affected. Go to the z/OS File System Mapping View
Select the mapping for the playback file if it exists, or right click in the view and select Add Data Set Mapping if it does not. Remember, right at the beginning, in our preferences, we select the name that would be used to create the playback file. So the mapping we have created aligns with that:
We need to make sure that the transfer mode is set to Binary, and the Host Code Page used is an EBCDIC one, either IBM-037 or IBM-1047. Once we have checked our settings we can stop the recording by clicking the Record Data button again:
When the recording session is stopped the Filter Recorded Data dialog opens. So the next step is to select the transactions to import.
Click on the Import Selections button and select the RSE connection to the system where the playback file will be created and click Create, which will create the playback file in a z/OS data set and than import it to the local project:
Once the connection is selected and the playback file name is confirmed click Create and the playback file will be created on the z/OS system specified.
The playback file is created on z/OS, so we can check that by looking in our MVS files in the Remote Systems View:
In addition the playback file has also been imported into the local project:
Notice in the local project the playback file now exists but the test case program and the configuration file do not exist yet. So go back to our Test case editor view and select the Generate button:
The test case program and configuration file are now created in our local project:
Looking in our local Eclipse zProject we can now see the configuration file and test case module have been added:
Finally, checking the configuration file we can see the names of the program we are testing, the name of the testcase program that will be used to perform the test, and the remote and local names of the playback file. As EWM will store this local project in the repository, the remote name is not used, but the localName is what EWM will use when it invokes the zUnit runner.
Sharing, building and running the Test Case in EWM
If this is the first time we have worked on this project then we will need to Share it with the EWM repository where it will be stored. If you have already shared this project and have changed the test case program, playback file or config file, then you will need to check in your changes. Let’s assume this is the first time we have captured anything. So, the first thing we do is Share the project into our EWM workspace.
The next step is to associate all the zFolders to Data Set Definitions and zFiles to language Definitions. The data set definitions, Language Definitions and translators will be described in more detail later in the article.
Note : In the examples below the EWM system definitions are prefixed with LD-. There is no significance to this other than to allow uniqueness in the test repository I was using to capture the examples.
The zUnit configuration file is the part that will drive the process as this has the required information to pass to the runner that invokes the tests.
In the above example the project actually contains 2 COBOL programs. The program being tested, lgidb01.cbl in the COBOL zFolder , and the generated test case (program), Tlgicdb0.cbl, in the TESTPGM zFolder, plus the related zUnit artifacts required to test them.
The key points are that there are two COBOL programs involved in this example, the first is the actual program that needs to be tested, and the second one is the Test case program that drives the test. There are 3 main language definitions involved. The first compiles the actual COBOL program, and in this example, this is a COBOL/CICS/Db2 program. The second compiles the Test Case program, and that is just a normal COBOL compile, no CICS, no Db2. The third language definition is attached to the config file and will parse the config file to get the name of the two programs plus the name of the playback file if used. In the first release of zUnit we have to call it from a JCL as dynamic TASKLIB allocations, which EWM requires to call modules, are not possible in this release of zUnit. So the translator will set up the required allocations to call the Runner, and then invoke the Runner through a generated JCL.
Let’s look at the EWM system definitions.
Data set Definitions
These are the zUnit specific data set definitions that are required for zUnit in the EWM zFolders and translators. More information can be found in the zUnit Runner documentation. The name relates to the data set definition name used in the translators described in this article.
Name | Data set | Usage | RECFM | LRECL | BLKSIZE | Type |
LD-BZUCFG | BZUCFG | Destination data set for a zFolder | VB | 16383 | 32760 | Library(PDSE) |
LD-BZUPLAY | BZUPLAY | Destination data set for a zFolder | FB | 256 | Library(PDSE) | |
LD-BZULOAD | BZULOAD | Build output data set | U | 0 | 4096 | Library(PDSE) |
LD-BZUMSG | BZUMSG | Build output data set | VB | 133 | Library(PDSE) | |
LD-BZUOUT | BZUOUT | Build output data set | FB | 133 | 32718 | Library(PDSE) |
LD-BZURPT | BZURPT | Build output data set | VB | 16383 | 32760 | Library(PDSE) |
LD-SBZUPROC | BZU.PROCLIB | Existing data set used for build | ||||
LD-SBZUSAMP | BZU.SBZUSAMP | Existing data set used for build | ||||
LD-SBZULOAD | BZU.SBZULOAD | Existing data set used for build | ||||
LD-SBZULMOD | BZU.SBZULMOD | Existing data set used for build | ||||
LD-SBZULLEP | BZU.SBZULLEP | Existing data set used for build |
Do Nothing Language definition
Although this actually does nothing except to ensure files are transferred across to z/OS, we have to b careful with regard to the scanner that it uses. The Do Nothing language definition references a Do Nothing translator that calls IEFBR14 to ensure the playback files are transferred across to z/OS. By default a translator with be assigned the default z/OS scanner, and even if all the dependency types and translators are removed, the source will still be scanned. For the playback files, which can be quite large, this is a problem as it can cause a scan that takes a few seconds, to run for hours.
So, the scanner for the Do Nothing language definition needs to be set to the Registration Scanner to ensure no scanning is done.
COBOL CICS DB2 compilation and link-edit Language definition
This is just a regular COBOL/CICS/Db2 language definition and translator. It uses the integrated CICS and SQL pre-processors and uses these compiler options : LIB,CICS,SQL.
zUnit Test Case COBOL compilation and link-edit Language definition
Again, just a regular COBOL compile and link-edit language definition and translator for the Test Case program. In this example the COBOL compiler options of LIB,DYNAM were used. However, one thing to note is the SYSLIB concatenation. It needs to contain the zUnit SBZUSAMP data set as there are ZUnit copybooks that need to be included during the compile:
zUNIT-Config File Processor
This language definition is the cornerstone for the whole process. There are two important parts of this language definition to discuss. The first is the dedicated scanner and the second is the translator to parse the config file and run the zUnit runner.
zUnit Config File scanner
A new scanner has been provided with EWM 7.0.1 to scan the zUnit config file and pull out three pieces of information that need to be stored in the source code data. These are:
- The name of the zUnit playback file.
- The source name of the actual program being tested.
- The source name of the Test Case program.
When a scan is run, either manually or invoked by a build, the scanner will get the required information listed above, and add Source Code Data for the *.bzucfg members. This source code data will then be used by EWM impact analysis, such that if one of the programs changes, or the playback file changes, then the zUnit tests will be run. The scenarios that this may apply to are:
- The program being tested changes, but the test case is still valid. So you need to change and recompile the program and then make sure the test case(s) still run after the change.
- You capture some some new tests but the program being tested does not change. So you need to recompile the test case, and upload and deliver a new playback file and config file. Then run the test case.
The Source Code Data is shown below, make a note of the dependencyPath information, as this is what makes the connection to the other files.
zUnit Config File translator
As mentioned previously, in the version of zUnit shipped with IDz post v14.2.3 it is not possible to allocate TASKLIB to contain the zUnit Runner load library SBZULOAD. As such, this version of the article will discuss how the REXX exec will generate JCL to run the zUnit Runner. It should also be noted that with EWM 7.0.1 the BZUPLAY file must be stored as a FB Binary file, with a RECFM of 256 to maintain the format of the file.
The zUnit Config File translator uses the REXX exec to parse the config file to get the member names of the zUnit playback file, the load module name of the actual program and the load module name of the Test Case program. A copy of the REXX exec is included in the Appendices.
The translator will need to allocate all the required zUnit runner DD names, as per the zUnit documentation. In addition, there are the three DDs allocated to link to the Source Code Data, namely SYSPLAY, SYSPROG and SYSTEST. This will guarantee that if the zUnit Test program or the actual program being tested are changed and delivered, the config file language definition will run, and the zUnit tests will be run. The BZUPROC DD points to the zUnit product PROCLIB as shipped with the product.
The translator also allocates all the required zUnit runner DD names:
The zUnit Runner documentation should be consulted for detailed information on configuring and running the zUnit Runner program. However for reference these DDs are as follows:
BZUCFG | zUnit Configuration File. This will be parsed to run the zUnit test |
BZUPLAY | zUnit Playback file |
BZUCBK | zUnit Call back library. This is the library where the zUnit test programs were link-edited into |
BZULOD | Load library containing the programs to be tested |
BZUMSG | zUnit messages |
BZURPT | zUnit Results report |
SYSOUT | zUnit additional debug messages |
The translator calls a REXX exec, BZUCFJCL, that parses the config file to extract the names of the load module being tested, the Test Case load module and the playback file to use. This allows flexibility in the naming of the files that the zUnit capture tooling uses. The REXX then generates a JCL member that allocates the required files and calls the zUnit runner to run the Test Case. A test is made to check the return code from the zUnit runner and the language definition will either pass or fail based on the return code from the zUnit Runner program.
The complete translator is shown below:
Running the build
The build definition is a standard EWM z/OS Dependency build that includes the five language definitions described above:
The zUnit scanner captures dependency data from the configuration file, as described previously. So, if, for example, just the main COBOL program is changed, and a Build Changed Items Only build is run then EWM will compile the changed COBOL program, but will also rerun the zUnit Test Case:
The JCL that is generated by the REXX runs the test case and returns a good or bad return code to indicate if the test worked or failed. The generated JCL is as follows:
//DOHERTLZ JOB ,MSGCLASS=X,CLASS=A,NOTIFY=&SYSUID,REGION=0M
// JCLLIB ORDER=(DEMO.ZUNIT.PROCLIB)
//*
//BADRC EXEC PGM=IEFBR14
//DD DD DSN=&SYSUID..BADRC,DISP=(MOD,CATLG,DELETE),
// DCB=(RECFM=FB,LRECL=80),UNIT=SYSALLDA,
// SPACE=(TRK,(1,1),RLSE)
//*
//* Action: Run Test Case…
//* Source: DOHERTL.ZUNIT3.BZULOAD(TLGICDB0)
//RUNNER EXEC PROC=BZUPPLAY,
// BZUCFG=DOHERTL.ZUNIT3.BZUCFG(LGICDB01),
// BZUCBK=DOHERTL.ZUNIT3.BZULOAD,
// BZULOD=DOHERTL.ZUNIT3.LOAD,
// PARM=(‘STOP=E,REPORT=XML’)
//BZUPLAY DD DISP=SHR,
// DSN=DOHERTL.ZUNIT3.BZUPLAY(LGICDB01)
//BZURPT DD DISP=SHR,
// DSN=DOHERTL.ZUNIT3.BZURPT(LGICDB01)
//BZUMSG DD DISP=SHR,
// DSN=DOHERTL.ZUNIT3.BZUMSG(LGICDB01)
//SYSOUT DD DISP=SHR,
// DSN=DOHERTL.ZUNIT3.BZUOUT(LGICDB01)
//*
//IFGOOD IF RC=0 THEN
//GOODRC EXEC PGM=IEFBR14
//DD DD DSN=&SYSUID..BADRC,DISP=(MOD,DELETE,DELETE),
// DCB=(RECFM=FB,LRECL=80),UNIT=SYSALLDA,
// SPACE=(TRK,(1,1),RLSE)
// ENDIF
Appendices
REXX exec to parse and run the zUnit Runner
The REXX exec used, in this example called BZUCFJCL is not part of the Eclipse project being tested. It is a release engineering artifact, so as such it could be stored in a separate Eclipse project, or just stored in a PDS(E) on z/OS. This is the REXX that is called:
/* REXX */ "EXECIO * DISKR BZUCFG (STEM bzucfg. FINIS)" Address ISPEXEC "QBASELIB BZUPLAY ID(BZUPLAY)" Parse var BZUPLAY "'" BZUPLAY "'" testCaseFound = 0 playFileFound = 0 loadModFound = 0 /* Parse the config file and look for play, load */ Do i = 1 to bzucfg.0 Select When (Pos('<runner:testCase',bzucfg.i) > 0) Then Do Parse var bzucfg.i '<runner:testCase' . 'moduleName="'testMod'">' testMod = Translate(testMod) testCaseFound = 1 End When (Pos('<runner:playback',bzucfg.i) > 0) Then Do Parse var bzucfg.i '<runner:playback' . 'moduleName="'loadMod'">' loadMod = Translate(loadMod) loadModFound = 1 End When (Pos('<playbackFile name',bzucfg.i) > 0) Then Do Parse var bzucfg.i '<playbackFile name="' . , 'localName="'playFile'.plbck"/>' playFile = Translate(playFile) playFileFound = 1 End Otherwise Nop End End If testCaseFound + playFileFound + loadModFound /= 3 Then Do If testCaseFound = 0 Then Say 'Testcase program not found' If playFileFound = 0 Then Say 'Playback file not found' If loadModFound Then Say 'Load module not found' Exit 8 End Address TSO "Free f(BZUPLAY)" Address ISPEXEC "QBASELIB BZUPROC ID(BZUPROC)" Parse var BZUPROC "'" BZUPROC "'" Address ISPEXEC "QBASELIB BZUCFG ID(BZUCFG)" Parse var BZUCFG "'" BZUCFG "'" Address ISPEXEC "QBASELIB BZUCBK ID(BZUCBK)" Parse var BZUCBK "'" BZUCBK "'" Address ISPEXEC "QBASELIB BZULOD ID(BZULOD)" Parse var BZULOD "'" BZULOD "'" Address ISPEXEC "QBASELIB BZUMSG ID(BZUMSG)" Parse var BZUMSG "'" BZUMSG "'" Address ISPEXEC "QBASELIB BZURPT ID(BZURPT)" Parse var BZURPT "'" BZURPT "'" Address ISPEXEC "QBASELIB SYSOUT ID(SYSOUT)" Parse var SYSOUT "'" SYSOUT "'" job.0 = 30 job.1 = "//"Userid()"Z JOB ,MSGCLASS=X,CLASS=A,NOTIFY=&SYSUID,REGION=0M" job.2 = "// JCLLIB ORDER=("BZUPROC")" job.3 = "//*" job.4 = "//BADRC EXEC PGM=IEFBR14" job.5 = "//DD DD DSN=&SYSUID..BADRC,DISP=(MOD,CATLG,DELETE)," job.6 = "// DCB=(RECFM=FB,LRECL=80),UNIT=SYSALLDA," job.7 = "// SPACE=(TRK,(1,1),RLSE)" job.8 = "//*" job.9 = "//* Action: Run Test Case..." job.10 = "//* Source: "BZUCBK"("testMod")" job.11 = "//RUNNER EXEC PROC=BZUPPLAY," job.12 = "// BZUCFG="BZUCFG"," job.13 = "// BZUCBK="BZUCBK"," job.14 = "// BZULOD="BZULOD"," job.15 = "// PARM=('STOP=E,REPORT=XML')" job.16 = "//BZUPLAY DD DISP=SHR," job.17 = "// DSN="BZUPLAY"("playFile")" job.18 = "//BZURPT DD DISP=SHR," job.19 = "// DSN="BZURPT job.20 = "//BZUMSG DD DISP=SHR," job.21 = "// DSN="BZUMSG job.22 = "//SYSOUT DD DISP=SHR," job.23 = "// DSN="SYSOUT job.24 = "//*" job.25 = "//IFGOOD IF RC=0 THEN" job.26 = "//GOODRC EXEC PGM=IEFBR14" job.27 = "//DD DD DSN=&SYSUID..BADRC,DISP=(MOD,DELETE,DELETE)," job.28 = "// DCB=(RECFM=FB,LRECL=80),UNIT=SYSALLDA," job.29 = "// SPACE=(TRK,(1,1),RLSE)" job.30 = "// ENDIF" Address TSO "ALLOC F(TEMPSUB) SPACE(1,10) TRACKS DSORG(PS) RECFM(F,B) " ||, "LRECL(80) BLKSIZE(0) UNIT(SYSALLDA)" Address TSO "EXECIO "job.0" DISKW TEMPSUB (FINIS STEM job." Address ISPEXEC "QBASELIB TEMPSUB ID(TEMPSUB)" X = OUTTRAP('smsg.') Address TSO "SUBMIT "TEMPSUB Address TSO "FREE F(TEMPSUB)" /* Need to make sure job is finished */ x = OUTTRAP('OFF') If smsg.0 = 0 then Do Say 'Unable to get job number' End Do i = 1 to smsg.0 say smsg.i parse var smsg.i id1 job jobn'('jobid')' . End jobRunning = 1 Do while (jobRunning) x = OUTTRAP('status.') JOBPARM = jobn'('jobid')' Address TSO 'STATUS 'JOBPARM STATrc = rc y = OUTTRAP('OFF') Do i = 1 to status.0 If Pos('ON OUTPUT QUEUE',status.i) > 0 Then jobRunning = 0 End End /* Check if BADRC data set exists. If so set an rc=8 */ x = msg('off') Address TSO "ALLOC F(BADRC) DA(BADRC) SHR" x = msg('on') If rc = 0 Then rc = 8 Else rc = 0 Exit rc |
© Copyright IBM Corporation 2020