Mocking In RPGLE Unit Tests
We’ve been using RPGUNIT to write unit tests for around 5 years now, and we’re constantly updating it to ensure we can get feedback on whether our code is working as quickly as possible.
There are times, however, were we decide that unit testing in RPGUNIT is just too hard, and we fall back to manual testing.
But I don’t like to be beaten….
We had a program (Lets call it RESTPGM) that used Scott Klement’s excellent HTTPAPIR4 *SRVPGM to make a call to a webservice, but it had been implemented without any kind of retry functionality, which was a pain, as we had one or 2 failures a day, presumably caused by network issues.
There was talk amongst the team of writing our own version of the web service to simulate the comms issues, but I thought that there must be a way to do this as an automated unit test. If we were writing a Java application, we would have just mocked out the HTTP request to give the desired response, but there’s no way to do that in RPG, right?
Wrong.
Option 1 - Create a Mock version of HTTPAPIR4.
- Create a file M_HTTPDTA to hold the HTTP Response code, HTTP Response, and HTTP_ERROR text of the requests being tested.
- Create *SRVPGM M_HTTP which is an exact copy of HTTPAPIR4 and binds in all the same *MODULES and, most importantly, exports the same signature.
- Create *MODULE M_HTTP1 , add the procedure to mock (in this case HTTP_URL_POST_XML), and bind it into M_HTTP.
*********************************************************************** P http_url_post_xml... P B export D http_url_post_xml... D PI 10I 0 D peURL 32767A varying const options(*varsize) D pePostData * value D pePostDataLen 10I 0 value D peStartProc * value procptr D peEndProc * value procptr D peUsrDta * value D peTimeout 10I 0 value options(*nopass) D peUserAgent 64A const options(*nopass:*omit) D peContentType 64A const options(*nopass:*omit) D peSOAPAction 64A const options(*nopass:*omit) D response s 8192a /free if not %open(M_HTTPDATA); open M_HTTPDATA; endif; read M_HTTPDATA; if %eof(M_HTTPDATA); SetError(HTTP_RDWERR:'No Mocking Data Found For Request!'); return -999; endif; if M_ERRORTEXT <> ' '; SetError(HTTP_RDWERR:M_ERRORTEXT); endif; response = M_RESPBODY; if response <> *blanks; http_parse_xml_string(%addr(response):%len(response): 37:peStartProc:peEndProc:peUsrDta); endif; delete M_HTTPDATR; return M_RESPCODE; /end-free P E ***********************************************************************
- Create a procedure useMockingObject() to copy the mocking object into the unit test data library and rename mocking object.
*********************************************************************** p useMockingObject... p b export *********************************************************************** d pi d p_mockObject... d 10a const d p_replacingObject... d 10a const d p_replacingObjectType... d 10a const /free writeToConsole('Mocking ' + %trim(p_replacingObjectType)+ ' ' + %trim(p_replacingObject) + ' with ' + %trim(p_mockObject)); if objExists(p_replacingObject:getUnitTestDataLibraryName() :p_replacingObjectType); runCLCommand('DLTOBJ OBJ(' +%trim(getUnitTestDataLibraryName())+ '/' + %trim(p_replacingObject)+') OBJTYPE(' + %trim(p_replacingObjectType) + ')'); endif; runCLCommand('CRTDUPOBJ OBJ(' + %trim(p_mockObject) + ') ' +'FROMLIB(*LIBL) OBJTYPE(' + %trim(p_replacingObjectType) + ') ' + 'TOLIB(' +%trim(getUnitTestDataLibraryName()) + ') ' + 'NEWOBJ(' + %trim(p_replacingObject) + ') ' ); /end-free p e ***********************************************************************
- In the setupSuite() of the test, copy *SRVPGM M_HTTP into the unit test data library (Which is always at the top of the library list), and rename it to be HTTPAPIR4
********************************************************************** p setupSuite... p b export d pi ************************************************************************* * This is the procedure that should run first, to build the test database ************************************************************************* /free // Only if the flag is set at the top of the program: if g_useTestLibrary; // This is standard setup for using a test library, you shouldn't need // to touch it: if not isReusingExistingUnitTestDataLibrary(); buildTestDatabase(); generateNewUnitTestJournalReceiver(); elseif isIncrementallyAddingFiles(); addFilesToTestDatabase(); removeUnitTestJournalledChanges(); else; removeUnitTestJournalledChanges(); endif; // It is intended that the test database remains active for the duration // of the test: enableUnitTestDataLibrary(); useMockingObject('M_HTTP':'HTTPAPIR4':'*SRVPGM'); endif; ***********************************************************************
- In the test, populate M_HTTPDTA with the required response data, and call RESTPGM to make the request.
************************************************************************ p test_gskitHandshakeThenFine... p b export d pi ************************************************************************ /free // Add first failing response: executeSqlStatement('insert into M_HTTPDATA (respcode, + respbody, errortext) values(-1, + '' '',''SSL Handshake: (GSKit) Peer not + recognized or badly formatted message + received.'')'); // Followed By Successful response: executeSqlStatement('insert into M_HTTPDATA (respcode, + respbody, errortext) values(200, + ''<?xml version="1.0" encoding="UTF-8"?>+ <result type="array">+ <value type="dict">+ <email type="string">test@test.com</email>+ <status type="string">sent</status>+ <_id type="string">testid</_id>+ <reject_reason />+ </value>+ </result>'','''')'); // Call program to make request: RESTPGM(g_invoice); // Verify that 2nd response was processed correctly checkRecordInFile('RESTFILE':%char(g_invoice) + ' | *ANY | + '+g_testAddress+' | email: test@test.com, status: + sent, _id: testid, reject_reason: ,'); /end-free p e ************************************************************************
Option 2 - Use Procedure Pointers.
- Create *SRVPGM M_HTTP, which which this time ONLY contains the procedure MOCK_HTTP_URL_POST_XML and did pretty much the same as the mocking code in Option 1, but without being a complete replica of HTTPAPIR4.
- Change RESTPGM to accept an optional parameter of a procedure pointer, and use this pointer in preference to the pointer to HTTP_URL_POST_XML if it was passed.
************************************************************************ D doPost... D Pr 10I 0 extproc(postPointer) D peURL 32767A varying const options(*varsize) D pePostData * value D pePostDataLen 10I 0 value D peStartProc * value procptr D peEndProc * value procptr D peUsrDta * value D peTimeout 10I 0 value options(*nopass) D peUserAgent 64A const options(*nopass:*omit) D peContentType 64A const options(*nopass:*omit) D peSOAPAction 64A const options(*nopass:*omit) d postPointer s * procptr d inz(%paddr('HTTP_URL_POST_XML')) d PS29E pi d p_invoiceNumber... d 10a d p_mock_post... d * procptr const options(*nopass) /free if %parms >= %parmnum(p_mock_post); postPointer = p_mock_post; endif; doPost( 'https://myhost.com/api/1.0/messages/send-something.xml' : %addr(l_jsonMessage: *data) : %len(l_jsonMessage) : *null : %paddr(incoming) : *null : HTTP_TIMEOUT : HTTP_USERAGENT : 'application/json; charset=utf-8'); *********************************************************************
- In the test, populate M_HTTPDTA with the required data, and call RESTPGM to make the request.
************************************************************************ p test_gskitHandshakeThenFine... p b export d pi ************************************************************************ /free // Add first failing response: executeSqlStatement('insert into M_HTTPDATA (respcode, + respbody, errortext) values(-1, + '' '',''SSL Handshake: (GSKit) Peer not + recognized or badly formatted message + received.'')'); // Followed By Successful response: executeSqlStatement('insert into M_HTTPDATA (respcode, + respbody, errortext) values(200, + ''<?xml version="1.0" encoding="UTF-8"?>+ <result type="array">+ <value type="dict">+ <email type="string">test@test.com</email>+ <status type="string">sent</status>+ <_id type="string">testid</_id>+ <reject_reason />+ </value>+ </result>'','''')'); // Call program to make request using mock procedure: RESTPGM(g_invoice:%paddr('mock_http_url_post_xml')); // Verify that 2nd response was processed correctly checkRecordInFile('RESTFILE':%char(g_invoice) + ' | *ANY | + '+g_testAddress+' | email: test@test.com, status: + sent, _id: testid, reject_reason: ,'); /end-free p e ************************************************************************
So there you have it - I still can’t decide which method I prefer - Option 1 feels more elegant in that it doesn’t require passing pointers round, which would be especially annoying if you had to pass it down through 2 or 3 program calls, but it also feels a bit more fragile as the mock object’s signature will always have to match that of HTTPAPIR4.
Ultimately I think that it will depend on the code you’re trying to test and/or mock, but having these 2 techniques in your arsenal will hopefully mean that you CAN test that code that you’d previously assumed was untestable.