Article
· Mar 16 4m read

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:

Scenario:

A unit test class is created for each inbound feed based on the data profiling, routing, and mapping requirements.

Sample requirements:

Functional 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 serviceRouting RulesTransformation.

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.TESTAdtFromTESTAdt.ReorgSegmentProcessRouteTESTAdtTEST.ADTTransformHS.Gateway.HL7.InboundProcess

HL7 Production

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:

Unit Test Class 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
{
<test><![CDATA[
MSH|^~\&|Epic|TEST||TEST|20230911060119|RUBBLE|ADT^A01|249509431|P|2.2
EVN|A01|20230911060119||ADT_EVENT|RUBBLE^MATTHEW^BARNEY^D^^^^^TEST^^^^^UH|20230911060000|PHL^PHL^ADTEDI
ZVN||LS23450^LS-23450^^^^RTL
PID|1|000163387^^^MRN^MRN|000163387^^^MRN^MRN||FLINTSTONE^ANNA^WILMA||19690812|F|MURRAY^ANNA~FLINTSTONE^ANNA^R~FLINTSTONE^ANNA|B|100 BEDROCK WAY^^NORTH CHARLESTON^SC^29420-8707^US^P^^CHARLESTON|CHAR|(555)609-0969^P^PH^^^555^6090969~^NET^Internet^ANNAC1@YAHOO.COM~(555)609-0969^P^CP^^^555^6090969||ENG|M|CHR|1197112023|260-61-5801|||1|||||Non Veteran|||N
ZPD||MYCH||AC|||N||N
PD1|||TEST HOLLINGS CANCER CENTER^^10003|1134107873^LINK^MICHAEL^J^^^^^EPIC^^^^PNPI
ROL|1|UP|GENERAL|1134107873^LINK^MICHAEL^J^^^^^EPIC^^^^PNPI|20211115
NK1|1|GABLE^BETTY|PARENT||(555)763-5651^^PH^^^555^7635651||Emergency Contact 1
NK1|2|FLINTSTONE^FRED|Spouse|100 Bedrock way^^REMBERT^SC^29128^US|(888)222-2222^^PH^^^888^2222222|(888)222-3333^^PH^^^888^2223333|Emergency Contact 2
PV1|1|O|R1OR^RTOR^07^RT^R^^^^TEST RT OR|EL|||1386757342^HALSTEAD^LUCINDA^A.^^^^^EPIC^^^^PNPI|1386757342^HALSTEAD^LUCINDA^A.^^^^^EPIC^^^^PNPI||OTO||||PHYS|||1386757342^HALSTEAD^LUCINDA^A.^^^^^EPIC^^^^PNPI|SO||BCBS|||||||||||||||||||||ADMCONF|||20230911060000
PV2||PRV||||||20230911||||HOSP ENC|||||||||N|N||||||||||N
ZPV||||||||||||20230911060000
OBX|1|NM|PRIMARYCSN|1|1197112023||||||F
AL1|1|DA|900525^FISH CONTAINING PRODUCTS^DAM|3|Anaphylaxis|20210823
AL1|2|DA|568^PEANUT^HIC|3|Anaphylaxis|20221209
AL1|3|DA|12753^TREE NUT^HIC|3|Anaphylaxis|20221209
AL1|4|DA|1193^TREE NUTS^DAM|3|Anaphylaxis|20130524
AL1|5|DA|1554^HYDROCODONE^HIC||Other|20210728
AL1|6|DA|3102^POLLEN EXTRACTS^HIC||Other|20201204
AL1|7|DA|11754^SHELLFISH DERIVED^HIC||Other|20210728
DG1|1|I10|Q85.02^Neurofibromatosis, type 2^I10|Neurofibromatosis, type 2||ADMISSION DIAGNOSIS (CODED)
DG1|2|I10|D33.3^Benign neoplasm of cranial nerves^I10|Benign neoplasm of cranial nerves||ADMISSION DIAGNOSIS (CODED)
DG1|3|I10|J38.01^Paralysis of vocal cords and larynx, unilateral^I10|Paralysis of vocal cords and larynx, unilateral||ADMISSION DIAGNOSIS (CODED)
DG1|4||^NF2 (neurofibromatosis 2) [Q85.02]|NF2 (neurofibromatosis 2) [Q85.02]||ADMISSION DIAGNOSIS (TEXT)
DG1|5||^Acoustic neuroma [D33.3]|Acoustic neuroma [D33.3]||ADMISSION DIAGNOSIS (TEXT)
DG1|6||^Unilateral complete paralysis of vocal cord [J38.01]|Unilateral complete paralysis of vocal cord [J38.01]||ADMISSION DIAGNOSIS (TEXT)
GT1|1|780223|FLINTSTONE^ANNA^WILMA^^^^L||100 BEDROCK WAY^^NORTH CHARLESTON^SC^29420-8707^US^^^CHARLESTON|(555)609-0969^P^PH^^^555^6090969~(555)763-5651^P^CP^^^555^7635651||19690812|F|P/F|SL|248-61-5801|||||^^^^^US|||Full
ZG1||||1
IN1|1|BL90^BCBS/STATE EMP^PLANID||BCBS STATE|ATTN CLAIMS PROCESSING^PO BOX 100605^COLUMBIA^SC^29260-0605||(800)444-4311^^^^^800^4444311|002038404||||20140101||NPR||FLINTSTONE^THOMAS^^V|Sp|19661227|3310 DUBIN RD^^NORTH CHARLESTON^SC^29420^US|||1|||||||||||||1087807|ZCS49984141|||||||M|^^^^^US|||BOTH
IN3|1|||2||20230911|20230911|RUBBLE^MATTHEW^BARNEY^D|||NOT|||||(800)999-0000^^^^^800^9990000~(888)444-5555^^^^^888^4445555
ZIN|||||||FLINTSTONE^THOMAS^^V|||||16871492
]]></test>
}

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(),"<w")
        continue:line=""
        continue:$Extract(line,1)="<" // ignore opening or closing XML tags and start CData tag
        continue:$Extract(line,1)="]" // ignore ]]> 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.

Unit Test in Management Portal

Clicking on each test link will display more information on the test:

UnitTest Results

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.

Discussion (0)1
Log in or sign up to continue