Guru: A Handy Function for Unit Testing
December 4, 2017 Paul Tuohy
I would like to share a technique I use for dealing with lists in an RPG unit test program. According to Wikipedia, “. . . unit testing is a software testing method by which individual units of source code, sets of one or more computer program modules together with associated control data, usage procedures, and operating procedures, are tested to determine whether they are fit for use.”
In the world of modern RPG, this translates to writing “test” programs to test a specific piece of code. For example, when I write a subprocedure, I will write a test program that calls the subprocedure, passing the relevant test data. Usually, these test programs make copious use of the DSPLY operation to prompt for input values and to display the response and/or returned parameters from the subprocedure call. (See The Nearly Forgotten DSPLY Operation.)
Using the DSPLY operation is fine for displaying single values. But what if one of the parameters is a list? In such a case, using DSPLY to loop through the elements in the list can become very cumbersome. The DSPLY operation can only display up to 52 characters and you have to press Enter after each value is displayed.
An alternative is to make use of the printf() C function, which prints a line at a time.
The example I am going to share with you is created from six source members:
Member | Description |
APIC001 | Module contains the API_listObjects() subprocedure (subprocedure being tested) |
APIC001_P | Copy member containing prototypes, templates and Named Constants for APIC001 |
ADPC001U | Unit test program for API_listObjects() |
STDAPIINFO | Copy member containing prototypes, templates and Named Constants for system APIs |
UNIT_P | Prototype of the unit_printLine subprocedure |
UNIT01 | The unit_printLine subprocedure |
Since printf() is going to be used in many test unit programs, it is a good idea to wrap it as a subprocedure in a service program. You never know, there may be other unit test subprocedures you may want to add to the service program.
The unit_printLine() Subprocedure
Here is the UNIT_P source member.
dcl-Pr unit_printLine extProc(*dclCase); text pointer value options(*string); end-Pr;
It contains the definition of the prototype for the unit_printLine() subprocedure. The parameter definition (text) specifies a pointer, passed by value and converted to a string. This means that we can pass any character data as a parameter and it will have a null terminator (hex ’00’) attached, to make it a string.
The UNIT01 source member contains the definition of the unit_printLine() subprocedure. Please refer to the callouts for details.
(A) ctl-Opt noMain option(*srcStmt: *noDebugIO) bnddir('QC2LE'); /include QRPGLESRC,UNIT_P dcl-Proc unit_printLine export; dcl-PI *n; text pointer value options(*string); end-Pi; (B) dcl-Pr printf extproc('printf'); template pointer value options(*string); msg pointer value options(*string: *nopass); end-Pr; (C) dcl-C NEWLINE x'15'; (D) printf ('%s' + NEWLINE : text); end-Proc;
(A) The CTL-OPT includes a binding directory keyword for the QC2LE binding directory. This allows us to use the printf() function.
(B) Declare the prototype for printf()
(C) A New Line character will be required, at the end of every text line shown
(D) Call the printf() function. The parameter value of (‘%s’ + NEWLINE) indicates that characters up to the first null character are to be printed, followed by a New Line
UNIT01 is compiled to a module and the module is included in the service program UNITTEST. An entry for the service program UNITTEST is included in the binding directory UNITTEST.
This means that any program that needs to make use of the unit_printLine() subprocedure, simply has to include a bndDir(‘UNITTEST’) entry in the CTL-OPT of the program.
Now that we have our list procedure in place, let’s look at an example of how we can use it.
Sample Subprocedure for Testing
The subprocedure API_listObjects(), found in the APIC001_P source member, returns a list of objects for a requested library.
dcl-ds b_objectList qualified template; object char(10); type char(10); text char(50); created date(*ISO); changed date(*ISO); end-Ds; dcl-Pr API_listObjects int(10) extProc(*dclCase); forLibrary varChar(10) const; list likeDs(b_objectList) dim(9999); errorText varChar(80); end-Pr;
The APIC001_P source member contains the definition of the b_objectList data structure template and the prototype for the API_listObjects() subprocedure. The b_objectList template provides the format of the list returned and the prototype indicates that the subprocedure returns a number (which is the number of rows in the result), has an in-out parameter of a library name and returns an array of object descriptions and error text.
In the downloadable code, you will find source members APIC001 and STDAPIINFO. These subprocedures are not germane to writing the unit test. I included them for completion. We would only be concerned about this code if the unit test showed we had a problem!
Here’s source member APIC001.
**FREE ctl-Opt noMain option(*srcStmt: *noDebugIO); /include QRPGLESRC,APIC001_P /include QRPGLESRC,stdAPIInfo dcl-Proc API_listObjects export; dcl-PI *N int(10); forLibrary varChar(10) const; list likeDs(b_objectList) dim(9999); errorText varChar(80); end-PI; dcl-Ds listHeader likeDS(base_listHeader) based(pListHeader); dcl-Ds OBJL0300 likeDS(base_OBJL0300) based(pOBJL0300); dcl-s i int(10); dcl-s forObjects char(20); dcl-s stamp char(17); dcl-C USERSPACE 'OBJECTLISTQTEMP '; errorText = ''; getUserSpace(USERSPACE: pListHeader: APIError); if (APIError.bytesAvail > 0); createUserSpace( USERSPACE : 'APILIST' : 1000000 : x'00' : '*ALL' : 'User Space for API Output' : '*YES' : APIError); if (APIError.bytesAvail > 0); errorText = 'Error on createUserSpace ' + APIError.msgID; return 0; endIf; getUserSpace(USERSPACE: pListHeader: APIError); if (APIError.bytesAvail > 0); errorText = 'Error on getUserSpace ' + APIError.msgID; return 0; endIf; endIf; forObjects = '*ALL ' + forLibrary ; listObjects( USERSPACE : 'OBJL0300' : forObjects : '*ALL ' : APIError); if (APIError.bytesAvail > 0); errorText = 'Error on listObjects ' + APIError.msgID; return 0; endIf; if (listHeader.numberInList > %elem(list)); errorText = 'Too many objects in result list.'; return 0; endIf; pOBJL0300 = pListHeader + listHeader.offsetToList; for i = 1 to listHeader.numberInList; list(i).object = OBJL0300.object; list(i).type = OBJL0300.type; list(i).text = OBJL0300.text; convertDateTimeFormat('*DTS' :OBJL0300.createStamp : '*YYMD' :stamp :APIError); list(i).created = %date(%subst(stamp :1 :8) : *ISO0); convertDateTimeFormat('*DTS' :OBJL0300.changeStamp : '*YYMD' :stamp :APIError); list(i).changed = %date(%subst(stamp :1 :8) : *ISO0); pOBJL0300 += listHeader.entrySize; endFor; return listHeader.numberInList; end-Proc ;
This is the STDAPIINFO copy member, which is referenced in APIC001. Again, it is not germane to writing the unit test, and is included only for completeness.
**FREE // Standard API Error data structure used with most APIs dcl-Ds APIError qualified; bytesProvided int(10) inz(%size(APIError)); bytesAvail int(10) inz(0); msgID char(7); *N char(1); msgData char(240); end-Ds; // Create User Space dcl-Pr createUserSpace extPgm('QUSCRTUS'); userSpaceName char(20) const; attribute char(10) const; size int(10) const; initial char(1) const; authority char(10) const; text char(50) const; // Optional Parameter Group 1 replace char(10) const options(*noPass); errorCode LikeDS(APIError) options(*noPass); // Optional Parameter Group 2 domain char(10) const options(*noPass); // Optional Parameter Group 3 transferSize int(10) const options(*noPass); optimumAlign char(1) const options(*noPass); end-Pr; // Retrieve pointer to User Space dcl-Pr getUserSpace extPgm('QUSPTRUS'); userSpaceName char(20) const; pSpacePtr pointer; errorCode options(*noPass) likeDS(APIError); end-Pr; // List Object (Simple) dcl-Pr listObjects extPgm('QUSLOBJ'); userSpace char(20) const; formatName char(10) const; objectLibrary char(20) const; objectType char(10) const; errorCode likeDS(APIError); end-Pr; // Convert Date and Time Formats dcl-Pr convertDateTimeFormat extPgm('QWCCVTDT'); inputFormat char(10) const; input char(17) const options(*varSize); outputFormat char(10) const options(*varSize); output char(17) options(*varSize); errorCode likeDS(APIError); end-Pr; //--------------------------------------------------------------- // Base Formats //--------------------------------------------------------------- //--------------------------------------------------------------- // Generic API List Formats //--------------------------------------------------------------- // API List Header dcl-Ds base_listHeader qualified template; userArea char(64); headerSize int(10); releaseLevel char(4); format char(8); APIUsed char(10); created char(13); status char(1); userSpaceSize int(10); offsetToInput int(10); sizeOfInput int(10); offsetToHeader int(10); sizeOfHeader int(10); offsetToList int(10); sizeOfList int(10); numberInList int(10); entrySize int(10); entryCCSID int(10); countryID char(2); languageID char(3); subsettedList char(1); *N char(42); // Only use for ILE entryPointName char(256); *N char(128); end-Ds; //--------------------------------------------------------------- // QUSLOBJ Formats //--------------------------------------------------------------- dcl-Ds base_OBJL0300 qualified template; object char(10); library char(10); type char(10); status char(1); extendedAttribute char(10); text char(50); userDefinedAttribute char(10); *N char(7); ASP int(10); owner char(10); domain char(2); createStamp char(8); changeStamp char(8); storage char(10); compression char(1); allowChange char(1); changed char(1); auditingvalue char(10); digitallySigned char(1); trustedSource char(1); multiSign char(1); *N char(2); libraryASP int(10); end-Ds;
APIC001 is compiled using the CRTRPGMOD command.
The Unit Test Module
Here’s the source code for the Unit Test Module APIC001U. Please refer to the callouts for details.
**FREE (A) ctl-Opt option(*srcStmt: *noDebugIO) bndDir('UNITTEST'); (B) /include QRPGLESRC,APIC001_P (C) /include QRPGLESRC,UNIT_P (D) dcl-s libName varChar(10); dcl-s errorText varChar(80); dcl-s showText char(32); dcl-s i int(10); dcl-s numRows int(10); dcl-ds list likeDs(b_objectList) dim(9999); *inLR = *on; (E) dsply 'Library: ' ' ' libName; (F) numRows = API_listObjects(libName: list: errorText); (G) if (errorText <> ''); showText = errorText; dsply showText; else; for i = 1 to numRows; (H) unit_printLine(list(i).object + ' ' + list(i).type + ' ' + list(i).text + ' ' + %char(list(i).created: *ISO) + ' ' + %char(list(i).changed: *ISO)); endFor; endIf;
(A) The CTL-OPT includes a binding directory keyword for the UNITTEST binding directory. This allows us to use the unit_printLine() function.
(B) Include the prototype for the subprocedure we are testing (API_listObjects() )
(C) Include the prototype for the unit test routines (unit_printLine() )
(D) Declare any variables that will be required for testing e.g. parameters required on call to subprocedure being tested.
(E) Prompt for a library name
(F) Call the subprocedure being tested (API_listObjects() )
(G) If the error text contains data, display it.
(H) Otherwise, loop through the returned list and use unit_printLine() to print/display the contents of each item in the list.
APIC001U is compiled using the CRTRPGMOD command.
Creating The APIC001U Program
We now have two modules – APIC001U contains the unit test and APIC001 contains the subprocedure being tested. We create the unit test program using the command:
CRTPGM PGM(APIC001U) MODULE(APIC001U APIC001 UNIT01) ACTGRP(*NEW)
APIC001U must be the first module in the list of modules. ACTGRP(*NEW) must be specified because the list will not be displayed until the activation group ends.
Using The APIC001U Program
If we call the APIC001U program and provide the name of a library that does not exist, we should see the contents of the error text.
If we call the APIC001U program and provide the name of a library that does exist, we should see the contents of the returned list. We can page up and down through the list – just press enter when we are done.
More On Unit Testing
This is a simple example of a unit test program that displays a list. We could have written a more complicated unit test program that, instead of prompting for the library name, had a list of libraries and looped through the test for each library.
If you are interested in automating some of your unit testing, you might want to have a look at the RPGUnit open source project at https://github.com/takshil/RPGUnit.
If you are speaking about unit testing then you should always do it in an automated way so you can repeat the test at a later time.
Mentioning the old RPGUnit project on github is a not very well researched info. The github project is just a clone of the code from the original Sourceforge.net site. Nothing added.
A pointer to the RPGUnit branch from Thomas Raddatz would have been better. It supports exporting the test result (I think to a userspace) so that the test can be executed from RDi and the result displayed in a separted view in RDi. See http://www.tools400.de/rpgunit/update/rdp8.0/ .
Then there is the ILEUnit project ( https://bitbucket.org/m1hael/ileunit ) which goes one step further. It supports exporting the test result to a userspace and also to a stream file in the JUnit Test Report format so that it can be processed by tools like the JUnit Plugin for Jenkins. This is one piece of the puzzle when it comes to continuous development and continuous integration. For some info about it: http://wiki.rpgnextgen.com/doku.php?id=ibm_i_and_continuous_integration . The ILEUnit project is open source and already usable but also still work in progress. Feel free to contribute.