We use cookies
Agree
Pavel rozhkov, ilya Beda

Clinical Form management with Kaitenzushi

Beda EMR is driven by SDC IG. We leverage FHIR Questionnaires to build a full-featured clinical workflow engine. The workflow definition comprises a Questionnaire and Mapper that transforms users's answers into a set of FHIR resources. We leverage this approach for any forms and workflows in the system.
All interaction with users is implemented via FHIR Questionnaires.

Implementing core workflow features on top of user-defined data leaves us with new challenges. Questionnaires are not part of the system source code. They could be changed at runtime. However, they are still a code that should be maintainable and well-tested.

By design, Beda EMR could be launched on top of any FHIR server with different SDC microservices. So we need to make sure that the workflow works in the particular installation. So tests should be data along with Questionnaire definition and Mapper. As a result, we ended up with the concept of a bundle that consists of a Questionnaire, a Mapper, and a set of tests that validate the system corrected in the deployed runtime. So any clinical workflow like booking an appointment, registering a new patient, or entering clinical notes will be defined as a bundle.
Workflow testing tooling
Beda EMR may use different FHIR backends and different SDC microservices, so we need to make sure that questionnaires work on the specific run-time installation. We can’t use standard approaches like jest, pytest. We can't rely on specific programming language runtime availability. Also running a custom testing code may be inappropriate for security reasons.
We checked what the FHIR community has already provided. There is a TestScript resource. Initially, it was designed to check that FHIR server implementation is compliant with specific requirements like AU Core, CarinBB, etc.
In our use case, we need to make sure that specific installation runs out workflow correctly. So TestScript perfectly matches our case.
TestScript Problem
If you ever tried to write more than 2 TestScript resources for real-life cases, you can see a few problems as a developer:

1. A lot of code repeats;
2. A huge amount of "service" structures;
3. In the end you will get a test which hard to read, understand, and support in the future.

The main goal in this problem is to create a tool that can provide the ability for developers to write simple and easy-to-maintain tests.
Implementation
The general reduction
The start point of the investigation was: is it possible to write FHIR resource in short manner? And answer was yes, you can do it with FSH (FHIR Shorthand). Community created a DSL (domain specific language) to definiting the content for IG (implementation guide). It is provide a way to describe FHIR resource more shorter than you will use general approach using YAML of JSON.

As an example I want to demonstrate you how you can describe simple TestScript resource in FHIR and FSH. Let’s describe a case when we want to fill the Questionnaire and create HealthcareService resource from this questionnaire.

{
  "resourceType": "TestScript",
  "id": "healthcare-service-create-questionnaire-ts-test",
  "url": "https://beda.software/TestScript/healthcare-service-create-questionnaire-ts-test",
  "name": "healthcare-service-create-questionnaire-ts-test",
  "status": "draft",
  "date": "2023-10-26",
  "publisher": "Beda Software",
  "contact": [
    {
      "name": "Support",
      "telecom": [
        {
          "system": "email",
          "value": "ilya@beda.software",
          "use": "work"
        },
        {
          "system": "email",
          "value": "pavel.r@beda.software",
          "use": "work"
        }
      ]
    }
  ],
  "fixture": [
    {
      "id": "healthcare-service-create-body",
      "resource": {
        "reference": "file://tests/resources/healthcare-service-create-extract-request-body.yaml"
      },
"autocreate": false,
      "autodelete": false
    },
    {
      "id": "healthcare-service-create-questionnaire",
      "resource": {
        "reference": "Questionnaire/healthcare-service-create"
      },
      "autocreate": false,
      "autodelete": false
    }
  ],
  "test": [
    {
      "name": "Create new encounter",
      "description": "Test extract operation",
      "action": [
        {
          "operation": {
            "type": {
              "code": "extract",
              "system": "http://terminology.hl7.org/CodeSystem/testscript-operation-codes"
            },
            "resource": "Questionnaire",
            "description": "Questionnaire data extract",
            "accept": "json",
            "method": "post",
            "targetId": "healthcare-service-create-questionnaire",
            "sourceId": "healthcare-service-create-body",
            "encodeRequestUrl": false
          }
        },
        {
          "assert": {
            "description": "Confirm that the extract returned HTTP status is 200(OK).",
            "direction": "response",
            "response": "okay",
            "warningOnly": false
          }
        }
      ]
    }
 ]
}

Alias: $testscript-operation-codes = http://terminology.hl7.org/CodeSystem/testscript-operation-codes

Instance: healthcare-service-create-questionnaire-ts-test
InstanceOf: TestScript
Usage: #example
* url = "https://beda.software/TestScript/healthcare-service-create-questionnaire-ts-test"
* name = "healthcare-service-create-questionnaire-ts-test"
* status = #draft
* date = "2023-10-26"
* publisher = "Beda Software"
* contact.name = "Support"
* contact.telecom[0].system = #email
* contact.telecom[=].value = "ilya@beda.software"
* contact.telecom[=].use = #work
* contact.telecom[+].system = #email
* contact.telecom[=].value = "pavel.r@beda.software"
* contact.telecom[=].use = #work
* fixture[0].id = "healthcare-service-create-body"
* fixture[=].resource = Reference(file://tests/resources/healthcare-service-create-extract-request-body.yaml)
* fixture[=].autocreate = false
* fixture[=].autodelete = false
* fixture[+].id = "healthcare-service-create-questionnaire"
* fixture[=].resource = Reference(Questionnaire/healthcare-service-create)
* fixture[=].autocreate = false
* fixture[=].autodelete = false
* test.name = "Create new encounter"
* test.description = "Test extract operation"
* test.action[0].operation.type = $testscript-operation-codes#extract
* test.action[=].operation.resource = #Questionnaire
* test.action[=].operation.description = "Questionnaire data extract"
* test.action[=].operation.accept = #json
* test.action[=].operation.method = #post
* test.action[=].operation.targetId = "healthcare-service-create-questionnaire"
* test.action[=].operation.sourceId = "healthcare-service-create-body"
* test.action[=].operation.encodeRequestUrl = false
* test.action[+].assert.description = "Confirm that the extract returned HTTP status is 200(OK)."
* test.action[=].assert.direction = #response
* test.action[=].assert.response = #okay
* test.action[=].assert.warningOnly = false
1. An example of the simple TestScript implementation FSH vs JSON
As you see, the FSH structure looks more simple and it is easy to understand. But there is a lot of repeat items, as a developers we want to follow the principle DRY (Don't repeat yourself). And of course this example is super unrealistic, because we want to check not only success extraction of the questionnaire. We also want to understand how much resources was created, what is content of these resources, etc...
Simplification
The next research question was: is it possible to write some kind of function like in python or javascript to encapsulate all test things and keep FSH api clean for the developer? The short answer is yes, we can do it with RuleSet and Alias.

Rule sets provide the ability to define a group of rules as an independent entity. Or in other case: we can create reusable pieces of code, which can get parameters like functions and return back a structure.

Let me show how we can describe some RuleSet to reducе line numbers and boost readability of the TestScript.

 insert AddTelecom("email", "ilya@beda.software", "work")

RuleSet: AddTelecom(system, value, use)
* contact.telecom[+]
  * system = #{system}
  * value = {value}
  * use = #{use}
2. Implementation and usage of the AddTelecom RuleSet

* insert AddFixtureFile("healthcare-service-create-body", healthcare-service-create-extract-request-body.yaml)
* insert AddFixtureResource("healthcare-service-create-questionnaire", Questionnaire/healthcare-service-create)

RuleSet: AddFixture(id, path)
* fixture[+].id = {id}
* fixture[=].resource.reference = "{path}"
* fixture[=].autocreate = false
* fixture[=].autodelete = false

RuleSet: AddFixtureFile(id, fileName)
* insert AddFixture({id}, file://tests/resources/{fileName})

RuleSet: AddFixtureResource(id, reference)
* insert AddFixture({id}, {reference})
3. Implementation and usage of the AddFixture RuleSet

RuleSet: CreateTest(name, description)
* test[+].name = {name}
* test[=].description = {description}

* insert CreateTest("Create new encounter", "Test extract operation")
4. Implementation and usage of the CreateTest RuleSet

RuleSet: TSTestOperationGlobal(type, resource, description, accept, method, targetId, sourceId)
* test[=].action[+].operation
  * type = $testscript-operation-codes#{type}
  * resource = #{resource}
  * description = {description}
  * accept = #{accept}
  * method = #{method}
  * targetId = {targetId}
  * sourceId = {sourceId}
  * encodeRequestUrl = false

RuleSet: TSTestAssertSuccessResponse
* test[=].action[+].assert
  * description = "Confirm that the extract returned HTTP status is 200(OK)."
  * direction = #response
  * response = #okay
  * warningOnly = false

RuleSet: ExtractQuestionnaire(targetId, sourceId)
* insert TSTestOperationGlobal("extract", "Questionnaire", "Questionnaire data extract", "json", "post", {targetId}, {sourceId})
* insert TSTestAssertSuccessResponse

* insert ExtractQuestionnaire("healthcare-service-create-questionnaire", "healthcare-service-create-body")
5. Implementation and usage of the ExtractQuestionnaire RuleSet

RuleSet: TSTestOperationSearch(description, type, resource, params)
* test[=].action[+].operation
  * description = {description}
  * type = $QuestionnaireResponse-extract#{type}
  * resource = #{resource}
  * encodeRequestUrl = true
  * params = {params}
  * encodeRequestUrl = false

RuleSet: SearchFHIRResources(resourceType, params)
* insert TSTestOperationSearch("Search FHIR resources", "search", {resourceType}, {params})

* insert SearchFHIRResources("HealthcareService", "?service-type=ecg")
6. Implementation and usage of the SearchFHIRResources RuleSet

RuleSet: TSTestAssertWithPropEmptySourceIdExpression(description, resource, value, operator, expression)
* test[=].action[+].assert
  * description = {description}
  * resource = #{resource}
  * value = {value}
  * operator = #{operator}
  * warningOnly = false
  * expression = "{expression}"

RuleSet: AssertEqualTo(resource, expression, value)
* insert TSTestAssertWithPropEmptySourceIdExpression("Check is equal", {resource}, {value}, "equals", [[{expression}]])

* insert AssertEqualTo("Bundle", [[Bundle.total.toString()]], "1")
7. Implementation and usage of the AssertEqualTo RuleSet
As the result we will get a TestScript described via FSH looks like:

Instance: healthcare-service-create-questionnaire-ts-test
InstanceOf: TestScript
Usage: #example
* url = "https://beda.software/TestScript/healthcare-service-create-questionnaire-ts-test"
* name = "healthcare-service-create-questionnaire-ts-test"
* status = #draft
* date = "2023-10-26"
* publisher = "Beda Software"
* contact.name = "Support"
* insert AddTelecom("email", "ilya@beda.software", "work")
* insert AddTelecom("email", "pavel.r@beda.software", "work")

* insert AddFixtureFile("healthcare-service-create-body", healthcare-service-create-extract-request-body.yaml)
* insert AddFixtureResource("healthcare-service-create-questionnaire", Questionnaire/healthcare-service-create)

* insert CreateTest("Create new encounter", "Test extract operation")
* insert ExtractQuestionnaire("healthcare-service-create-questionnaire", "healthcare-service-create-body")
* insert SearchFHIRResources("HealthcareService", "?service-type=ecg")
* insert AssertEqualTo("Bundle", [[Bundle.total.toString()]], "1")
Fuse it together
On this step we already understand that we can use FSH to provide a simple and flexible API for developers. For transform FSH to JSON we use SUSHI tooling. It provides a perfect functionality for it.

Also we want an ability to share FSH sources like RuleSets or Aliases between tests and projects (in the future). So, for this purpose we added an ability to setup dependency source via github. You can see current sharing dependencies problems in this FHIR chat topic. Kaitenzushi will get all FSH files from target repository and will add it on the translate step, which means that all predefined RuleSets and Aliases will be available in our TestScript.
Results
We made a wrapper on SUSHI which add an ability for developers to translate FSH files into the FHIR resources (YAML or JSON) with included dependencies such as RuleSets and Aliases. This functionality made a writting TestScript more comfortalble to write and maintendable to support.
What's next?
1. Describing RuleSets for questionnaire resources through FSH;
2. Development of repository of RuleSets (supported by community).
References
Get a free consultation
If you have any other questions about Kaitenzushi, please reach us at hi@beda.software and we'll be happy to chat.