Creating Unit Tests in ObjectScript for HL7 pipelines using %UnitTest class
One of the pain points for maintaining HL7 interfaces is the need to run a reliable regression test upon deployment to new environments and after upgrades. The %UnitTest class allows unit tests to be created and packaged alongside interface code. Test data can also be maintained within the unit test class, allowing for quick and easily repeatable smoke-testing and regression testing. Resources: Standard %UnitTest class This tutorial utilizes a modification of UnitTest-RuleSet by Alex Woodhead from the Open Exchange (https://openexchange.intersystems.com/package/UnitTest-RuleSet) to inject a message into a business service or process in a configured IRIS production. Scenario: A unit test class is created for each inbound feed based on the data profiling, routing, and mapping requirements. Sample requirements: To test each scenario, we need to run through a sample of each event type and confirm the routing rules. We also need to confirm the specific mapping requirements and find sample data for each scenario. HL7 Production The purpose for this UnitTest-RuleSet is to test a HL7 pipeline: Business service → Routing Rules → Transformation. In this example created for the production shown below, the ADT process flow goes through several additional hops. A class extending the UnitTest.RuleSet.HL7 was modified to allow for handling more complex process flows. Process Flow: FromHSROUTER.TESTAdt → FromTESTAdt.ReorgSegmentProcess → RouteTESTAdt → TEST.ADTTransform → HS.Gateway.HL7.InboundProcess Create Unit Test Class Create a TestAdtUnitTest class that extends UnitTest.RuleSet.HL7. (UnitTest.RuleSet.Example in the UnitTest-RuleSet repository contains an example of such a class. *Note: In this example, everything was moved under HS.Local in HSCUSTOM for ease of testing. * Signature: Class HS.Local.EG.UnitTest.TestAdtUnitTest Extends HS.Local.Util.UnitTest.RuleSet.HL7 Class Structure Preview The Unit Test class is organized with this layout: Set Up Parameters These class parameters are used to set up testing. // Namespace where production is located Parameter Namespace = "EGTEST"; // Base directory for unit tests Parameter TestDirectory = "/tmp/unittest"; // sub-directory name for unit tests Parameter TestSuite = "TEST-HL7ADT"; /// Override for different schema Parameter HL7Schema = "2.5.1:ADT_A01"; /// Override with name of existing service on production Parameter SourceBusinessServiceName = "FromHSROUTER.TESTAdt"; /// Override with name of existing business process routing engine on production Parameter TargetConfigName = "FromTESTAdt.ReorgSegmentProcess"; /// Primary routing process name on production Parameter PrimaryRoutingProcessName = "RouteTESTAdt"; /// Secondary routing process name on production Parameter SecondaryRoutingProcessName = "TEST.ADTTransform"; Note: The primary and secondary routing processes are referenced in various test methods in order to test the output and results from two routing processes chained together. Create Sample Input Messages To create a reusable Unit Test class, we save anonymized samples of each event type and any additional messages needed to fulfil the testing scenarios into the Unit Test class in XDATA blocks at the bottom of the file. Example of an XDATA block: The name SourceMessageA01 is used to reference the specific XDATA block. XData SourceMessageA01 { } Sample code to retrieve data from an XDATA block for use in testing: set xdata=##class(%Dictionary.CompiledXData).%OpenId(..%ClassName(1)_"||"_XDataName,0) Here’s the sample code from ** GetMessage**, a helper method used to read in and return the data from an XDATA block based on the block name. ClassMethod GetMessage(XDataName As %String) As EnsLib.HL7.Message { #dim SourceMessage as EnsLib.HL7.Message set xdata=##class(%Dictionary.CompiledXData).%OpenId(..%ClassName(1)_"||"_XDataName,0) quit:'$IsObject(xdata) $$$NULLOREF set lines="" while 'xdata.Data.AtEnd { set line=$ZSTRIP(xdata.Data.ReadLine(),"

One of the pain points for maintaining HL7 interfaces is the need to run a reliable regression test upon deployment to new environments and after upgrades. The %UnitTest class allows unit tests to be created and packaged alongside interface code. Test data can also be maintained within the unit test class, allowing for quick and easily repeatable smoke-testing and regression testing.
Resources:
Standard %UnitTest class
This tutorial utilizes a modification of UnitTest-RuleSet by Alex Woodhead from the Open Exchange (https://openexchange.intersystems.com/package/UnitTest-RuleSet) to inject a message into a business service or process in a configured IRIS production.
Scenario:
A unit test class is created for each inbound feed based on the data profiling, routing, and mapping requirements.
Sample requirements:
To test each scenario, we need to run through a sample of each event type and confirm the routing rules. We also need to confirm the specific mapping requirements and find sample data for each scenario.
HL7 Production
The purpose for this UnitTest-RuleSet is to test a HL7 pipeline: Business service → Routing Rules → Transformation.
In this example created for the production shown below, the ADT process flow goes through several additional hops. A class extending the UnitTest.RuleSet.HL7 was modified to allow for handling more complex process flows.
Process Flow: FromHSROUTER.TESTAdt → FromTESTAdt.ReorgSegmentProcess → RouteTESTAdt → TEST.ADTTransform → HS.Gateway.HL7.InboundProcess
Create Unit Test Class
Create a TestAdtUnitTest class that extends UnitTest.RuleSet.HL7. (UnitTest.RuleSet.Example in the UnitTest-RuleSet repository contains an example of such a class.
*Note: In this example, everything was moved under HS.Local in HSCUSTOM for ease of testing. *
Signature:
Class HS.Local.EG.UnitTest.TestAdtUnitTest Extends HS.Local.Util.UnitTest.RuleSet.HL7
Class Structure Preview
The Unit Test class is organized with this layout:
Set Up Parameters
These class parameters are used to set up testing.
// Namespace where production is located
Parameter Namespace = "EGTEST";
// Base directory for unit tests
Parameter TestDirectory = "/tmp/unittest";
// sub-directory name for unit tests
Parameter TestSuite = "TEST-HL7ADT";
/// Override for different schema
Parameter HL7Schema = "2.5.1:ADT_A01";
/// Override with name of existing service on production
Parameter SourceBusinessServiceName = "FromHSROUTER.TESTAdt";
/// Override with name of existing business process routing engine on production
Parameter TargetConfigName = "FromTESTAdt.ReorgSegmentProcess";
/// Primary routing process name on production
Parameter PrimaryRoutingProcessName = "RouteTESTAdt";
/// Secondary routing process name on production
Parameter SecondaryRoutingProcessName = "TEST.ADTTransform";
Note: The primary and secondary routing processes are referenced in various test methods in order to test the output and results from two routing processes chained together.
Create Sample Input Messages
To create a reusable Unit Test class, we save anonymized samples of each event type and any additional messages needed to fulfil the testing scenarios into the Unit Test class in XDATA blocks at the bottom of the file.
Example of an XDATA block:
The name SourceMessageA01 is used to reference the specific XDATA block.
XData SourceMessageA01
{
}
Sample code to retrieve data from an XDATA block for use in testing:
set xdata=##class(%Dictionary.CompiledXData).%OpenId(..%ClassName(1)_"||"_XDataName,0)
Here’s the sample code from ** GetMessage**, a helper method used to read in and return the data from an XDATA block based on the block name.
ClassMethod GetMessage(XDataName As %String) As EnsLib.HL7.Message
{
#dim SourceMessage as EnsLib.HL7.Message
set xdata=##class(%Dictionary.CompiledXData).%OpenId(..%ClassName(1)_"||"_XDataName,0)
quit:'$IsObject(xdata) $$$NULLOREF
set lines=""
while 'xdata.Data.AtEnd
{
set line=$ZSTRIP(xdata.Data.ReadLine()," closing CDATA
set lines=lines_($S($L(lines)=0:"",1:$C(..#NewLine)))_line
}
set SourceMessage=##class(EnsLib.HL7.Message).ImportFromString(lines,.tSC)
quit:$$$ISERR(tSC) $$$NULLOREF
set SourceMessage.DocType=..#HL7Schema
set tSC=SourceMessage.PokeDocType(..#HL7Schema)
quit SourceMessage
}
Create Testing Methods
The Unit Test class also contains a Test method that programmatically sets up each test, injects the message into the production, and asserts that the resulting transformed message matches what is expected.
The test example contains these test methods:
- TestMessageA01
- TestMessageA02
- TestMessageA03
- TestMessageA04
- TestMessageA05
- TestMessageA06
- TestMessageA08
- TestMessageA28
- TestMessageA31
- TestCorrectAssigningAuthorityForA01
- TestEncoutnerNumberPresent
- TestPD1LocationMapped
Example Method: TestMessageA01
Method to test that A01 messages process without error and are routed to correct transform.
Method TestMessageA01()
{
Set ReturnA01 = ..#SecondaryRoutingProcessName_":HS.Local.EG.ProfSvcs.Router.Base.ADT.TransformA01"
#dim message as EnsLib.HL7.Message
// Load new HL7Message per UnitTest
set ..HL7Message=..GetMessage("SourceMessageA01")
quit:'$IsObject(..HL7Message) $$$ERROR(5001,"Failed to correlate Xdata for Source Message")
set routingProcess = ..#PrimaryRoutingProcessName
set message=..HL7Message.%ConstructClone(1)
do message.PokeDocType(message.DocType)
do message.SetValueAt("SYSA","MSH:3.1")
set expectSuccess=1
set expectReturn="send:"_ReturnA01
set expectReason="rule#8"
do ..SendMessageToRouter(message,"TestMessageA01",routingProcess, expectSuccess, expectReturn, expectReason)
}
Note: SendMessageToRouter() is a method implemented in the UnitTest-RuleSet package.
Example Method: TestEncounterNumberPresent
The method below tests for specific resulting values in the message after the transform.
Method TestEncounterNumberPresent()
{
#dim message as EnsLib.HL7.Message
// Load new HL7Message per UnitTest
set ..HL7Message=..GetMessage("SourceMessageA01")
quit:'$IsObject(..HL7Message) $$$ERROR(5001,"Failed to correlate Xdata for Source Message")
//source of transform process
set routingProcess = ..#SecondaryRoutingProcessName
set message=..HL7Message.%ConstructClone(1)
do message.PokeDocType(message.DocType)
do message.SetValueAt("SYSA","MSH:3.1")
//Check that output has PV1:19 is not empty
set expectSuccess=1
set expectElement="PV1:19"
set expectReturnVal="1197112023"
do ..SendMessageReturnOutput(message,"TestMessageA01", routingProcess, expectSuccess, expectElement, expectReturnVal)
}
Note: SendMessageReturnOutput() is a method modified to return the output message for evaluation.
How to Run the UnitTest
The %UnitTest class implementation depends on the ^UnitTestRoot global for the location of the unit test working folders.
There are two parameters used to set the folders:
// Base directory for unit tests
Parameter TestDirectory = "/tmp/unittest";
// sub-directory name for unit tests
Parameter TestSuite = "TEST-HL7ADT";
In this case, the class would expect the folder: /tmp/unittest/TEST-HL7ADT to exist in the environment where the unit test class is run.
Additionally, the custom implementation sets the ^UnitTestRoot global and changes to the specific namespace in the Namespace parameter.
To run from the terminal:
HSCUSTOM> do ##class(HS.Local.EG.UnitTest.TestAdtUnitTest).Debug()
The results of the run will be sprinted to the console. If any test fails, the overall test is considered failed. In addition to the terminal, you can also see the results from the management portal. Use the link from the output to open the URL.
Snippet of Console Output
TestMessageA31 passed
TestPD1LocationMapped() begins ...
14:50:20.104:...t.TestAdtUnitTest: TestPD1Location Sent
LogMessage:SessionId was 130
LogMessage:Source Config Name name is :TEST.ADTTransform
LogMessage:Message Body Id was 94
LogMessage:Expect Success is 0
LogMessage:Testing for value PV1:39
LogMessage:Found value
AssertNotEquals:Expect No Match:TestPD1Location:ReturnValue= (failed) <<==== FAILED TEST-HL7ADT:HS.Local.EG.UnitTest.TestAdtUnitTest:TestPD1LocationMapped
LogMessage:Duration of execution: .033106 sec.
TestPD1LocationMapped failed
HS.Local.EG.UnitTest.TestAdtUnitTest failed
Skipping deleting classes
TEST-HL7ADT failed
Use the following URL to view the result:
http://192.168.1.229:52773/csp/sys/%25UnitTest.Portal.Indices.cls?Index=3&$NAMESPACE=EGTEST
Some tests FAILED in suites:
TEST-HL7ADT
Viewing Results in the Management Portal
Use the URL displayed on the terminal and paste it into a browser to see the results. Clicking on the test name will provide additional details.
Clicking on each test link will display more information on the test:
Conclusion
Building Unit Test classes for each HL7 interface and packaging it with the interface code provides documentation of the testing process and sample data and also allows for quick regression testing of any updates or changes to the data or implementation.
Upon deployment, the Unit Test class serves as a self-contained smoke test to confirm the interface is complete and functional.
Additional Considerations
The UnitTest_RuleSet framework can be adapted for CCDA unit testing. The routing tests are relatively similar to the HL7 example. To address the mapping requirements, the Unit Test class must be implemented to handle XSLT and XPaths.
QA resources can gather requirements and sample data to set up the Unit Test class before implementation begins.
Unit tests for all interfaces can be packaged together unter Unit Test Manager so that the entire package can be run at once to validate the existing configuration and code.