新しい投稿

Rechercher

記事
· 11 hr 前 7m read

API Unit Testing with CSP Requests and Reponses

In the previous article, we examined how we can use the %CSP.Request and %CSP.Response classes to test a REST API without having the API fully set up and accessible across a network with an authentication mechanism. In this article, we will build on that foundation to perform some simple unit testing of one of our REST API methods.

The unit testing framework requires a couple of setup steps before we can use it. First, we have to ensure that the unit testing portion of the management portal is enabled so we can review the results of our tests. It is located under System Explorer > Tools > UnitTest Portal. If it is not visible there, we should enter the following command into a terminal in the %SYS namespace to turn it on:

%SYS>set ^SYS("Security", "CSP", "AllowPrefix", "/csp/user/", "%UnitTest.")=1

Next, we have to set the global ^UnitTestRoot to a file path where we will store our test classes. After extending the %UnitTest.TestCase class, we must save the class file in this folder for the unit test manager to find it.. We should do it for the namespace where the tests will run. Remember to double up the slashes if you are on a Windows system. Check out the example below:

USER>set ^UnitTestRoot = “C:\\UnitTests”

Then, we should create a class extending the %UnitTest.TestCase in that folder. We will name it User.TestRequest.cls. Since test cases do not work like normal classes, you will not need to save and compile them into your IRIS instance. Instead, during the unit testing process, the class will be automatically compiled, the tests will be run, and the class will be removed from IRIS. It will not delete your test case file, but remove it from IRIS to save the space otherwise used for old unit tests.

We will define the methods in our class that start with the word "Test". It is the way for the unit testing framework to know what methods to run and test. You can also include any other supporting methods of your choice. However, the unit test will only directly call the methods starting with "Test". Your testing will be done with the various assert macros that the test case provides.

Macro

Description

$$$AssertEquals(value1,value2,description)

Passes if both provided values are equal

$$$AssertFailure(message)

Logs an unconditional failure with the given message

$$$AssertFilesSQLUnorderedSame(file1,file2,description)

Passes if the unordered SQL query results contained in the two files are identical

$$$AssertFilesSame(file1,file2,description)

Passes if two files are analogous

$$$AssertNotEquals(value1,value2,description)

Passes if the two provided values are not equal

$$$AssertNotTrue(value,description)

Passes if the boolean expression or value is not true

$$$AssertSkipped(message)

Logs that a test was skipped with the given message

$$$AssertStatusEquals(value1,value2,description)

Passes if the two provided %Status objects are equal

$$$AssertStatusNotOK(value,description)

Passes if the value of the %Status is not $$$OK

$$$AssertStatusOK(value,description)

Passes if the %Status provided is $$$OK

$$$AssertSuccess(message)

Logs an unconditional success with a message

$$$AssertTrue(value,description)

Passes if the Boolean value or expression is true

 

Almost all of the above-mentioned macros take multiple arguments, with the last one being a description that will show up in the unit testing portal. It helps you distinguish more clearly where each test belongs. A single method can include multiple tests by calling various macros. We will reuse the REST class we created in the previous article, focusing primarily on the CalcAge method. Below you can see this class again for reference:

Class User.REST Extends %CSP.REST
{
XData UrlMap [ XMLNamespace = "http://www.intersystems.com/urlmap" ]
{
    <Routes>
        <Route Url="/calc/age" Method="POST" Call="CalcAge" />
        <Route Url=”/echoname/:david” Method=”GET” Call=”EchoName” />
    </Routes>
}
ClassMethod CalcAge() As %Status
{
    try{
        set reqObj = {}.%FromJSON(%request.Content)
        set dob = reqObj.%Get("dob")
        set today = $P($H,",",1)
        set dobh = $ZDH(dob,3)
        set agedays = today - dobh
        set respObj = {}.%New()
        do respObj.%Set("age_in_days",agedays)
        set %response.ContentType = “application/json”
        write respObj.%ToJSON()
        return $$$OK
    }
    catch ex{
        return ex.AsStatus()
    }
}
ClassMethod EchoName(name As %String) As %Status
{
    try{
        if %request.Method '= "GET"{
            $$$ThrowStatus($$$ERROR($$$GeneralError,"Wrong method."))
        }
        write "Hello, "_name
    }
    catch ex{
        write "I'm sorry, "_name_". I'm afraid I can't do that."
    }
}
}

In our test case, we will now create a method to examine the CalcAge method. We will make it pick a hundred random numbers between 0 and the HOROLOG representation of today’s date. The two simplest things to check are whether the method returns a status of $$$OK and if the %response has an HTTP status of “200 OK”. We will also include two assertions for those tests, wrap them (the tests) in a try/catch block, and add an $$$AssertFailure in the exception-handling unit to collect any miscellaneous errors that will occur as the test runs. For instance, if our test method encounters any division by zero, it would catch and log these errors as unit test failures.

Method TestAge()
{
    for i=1:1:100{
        try{
            set today = $P($H,",",1)
            set dob = $RANDOM(today)
            set %request = ##class(%CSP.Request).%New()
            set %request.Content = ##class(%CSP.CharacterStream).%New()
            do %request.Content.Write("{""dob"":"""_$ZD(dob,3)_"""}")
            set %response = ##class(%CSP.Response).%New()
            do $$$AssertStatusOK(##class(User.REST).CalcAge(),"Method %Status Test")
            do $$$AssertEquals(%response.Status,"200 OK","HTTP Status Test")
        }
        catch ex{
            $$$AssertFailure(ex.DisplayString())
        }
    }
}

To execute our trial, we will save the class in the unit test root folder, then open a terminal session and execute the following command in the namespace where our unit test will need to run:

USER>do ##class(%UnitTest.Manager).RunTest()

After doing that, we can go into the System Management Portal and see the results of our unit tests:

By clicking a test's ID, we can check out detailed results. We can then click on (root)>the name of our test class>the name of the test method, and we will discover a table with a column called "actions". It shows which assertion was tested, its status (passed or failed), the description of the test we provided, and its location in our test case class.

It is a good start! Yet, suppose we want to keep digging in and verify that our method's output is correct. The calculated age should be equal to today’s HOROLOG date minus a randomly generated number. Although it is an easy calculation, the trick is capturing that output when the method is called. To do that, we will rely on the often-overlooked IO device, the spooler.

The spooler is simply a device named 2. When we use this device, instead of writing outputs directly from statements, they are written first to a global called ^SPOOL. Since we have a fairly simple method here, our output will end up in ^SPOOL(1,1). For more complex outputs, you may have to iterate over multiple subnodes within the global to get the entire response. The steps to do it are fairly simple. We will kill ^SPOOL to start fresh. Next, we will open 2 and use it. Then we will run our method, close 2, and create a dynamic object from the contents of ^SPOOL(1,1). From there on, the rest will look very familiar and comfortable. Let's add this to our test method.

Method TestAge()
{
    for i=1:1:100{
        try{
            set today = $P($H,",",1)
            set dob = $RANDOM(today)
            set %request = ##class(%CSP.Request).%New()
            set %request.Content = ##class(%CSP.CharacterStream).%New()
            do %request.Content.Write("{""dob"":"""_$ZD(dob,3)_"""}")
            set %response = ##class(%CSP.Response).%New()
            do $$$AssertStatusOK(##class(User.REST).CalcAge(),"Method %Status Test")
            do $$$AssertEquals(%response.Status,"200 OK","HTTP Status Test")
            kill ^SPOOL
            open 2
            use 2
            set sc = ##class(User.REST).CalcAge()
            close 2
            set json = ##class(%Library.DynamicObject).%FromJSON(^SPOOL(1,1))
            set age = json.%Get("age_in_days")
            do $$$AssertEquals(age,today - dob)
        }
        catch ex{
            $$$AssertFailure(ex.DisplayString())
        }
    }
}

We can tell whether our calculation was correct by using the spooler as mentioned above. Also, you might notice that we did not describe this test. When we see it in the unit test portal, it will have an automatically generated description outlining this test based on the assertion we utilized and the provided arguments.

If we wish to expand our testing, we have a few options. One would be creating a new class in the unit test root folder and adding methods there. We could also add more methods starting with the word "Test" to the existing class. In either case, the unit test manager will find and run those tests, but organize them differently in the unit test portal. It is my personal preference to have a separate test case class for each API class I am assessing, with a method in it for each method I am evaluating.

By learning to manipulate the %request and %response objects and understanding how to use those skills in your unit testing, you have gained a powerful new tool for your toolbox!

ディスカッション (0)1
続けるにはログインするか新規登録を行ってください
お知らせ
· 12 hr 前

[Video] Automating Provisioning and Management of FHIR Services

Hey Developers,

Watch this video to learn about automating the provisioning and management of FHIR services:

     ⏯ Automating Provisioning and Management of FHIR Services @ Ready 2025

The presentation introduces REST-based management APIs that enable automated provisioning, configuration, and lifecycle management of FHIR servers without relying on a UI. It explains how these APIs support infrastructure-as-code workflows, large-scale deployments, custom package management, and data loading through background jobs. A live demo illustrates creating, monitoring, customizing, populating, and deleting FHIR servers, highlighting how the same APIs are used by both scripts and the UI.

🗣 Presenter: Jaideep Majumdar, Development Manager at InterSystems

Want to see it in action? Watch the video and subscribe to see more demos!👍

ディスカッション (0)1
続けるにはログインするか新規登録を行ってください
お知らせ
· 12 hr 前

Beta Testers Needed for our Upcoming InterSystems ObjectScript Specialist Certification Exam

Hello DC community,

InterSystems Certification is currently developing a certification exam for ObjectScript developers, and if you match the exam candidate description below, we would like you to beta test the exam! The exam will be available for beta testing starting February 18th, 2026.

Beta testing will be completed May 4, 2026.

1件の新着コメント
ディスカッション (1)2
続けるにはログインするか新規登録を行ってください
お知らせ
· 16 hr 前

Meet Ashok Kumar - New Developer Community Moderator!

Hi Community,

Please welcome @Ashok Kumar T as our new Moderator in the Developer Community Team! 🎉

Let's greet Ashok with a round of applause and look at his bio!

@Ashok Kumar T is a Senior Software Engineer. 

A few words from Ashok: 

I'm a senior Software Engineer with over a decade of experience specializing in the InterSystems technology stack. Since 2014, my focus has been on leveraging the full power of the InterSystems ecosystem to solve complex data and integration challenges. I bring a deep understanding of both ObjectScript and modern IRIS implementations.

My professional philosophy is rooted in a commitment to core values: a constant willingness to learn and a proactive approach to sharing knowledge.

WARM WELCOME!

Thank you and congratulations, @Ashok Kumar T 👏

We're glad to have you on our moderators' team!

4件の新着コメント
ディスカッション (4)4
続けるにはログインするか新規登録を行ってください
記事
· 17 hr 前 6m read

pyprod: Pure Python IRIS Interoperability

Intersystems IRIS Productions provide a powerful framework for connecting disparate systems across various protocols and message formats in a reliable, observable, and scalable manner. intersystems_pyprod, short for InterSystems Python Productions, is a Python library that enables developers to build these interoperability components entirely in Python. Designed for flexibility, it supports a hybrid approach: you can seamlessly mix new Python-based components with existing ObjectScript-based ones, leveraging your established IRIS infrastructure. Once defined, these Python components are managed just like any other; they can be added, configured, and connected using the IRIS Production Configuration page. 


A Quick Primer on InterSystems IRIS Productions

Key Elements of a Production

Image from Learning Services training material

An IRIS Production generally receives data from external interfaces, processes it through coordinated steps, and routes it to its destination. As messages move through the system, they are automatically persisted, making the entire flow fully traceable through IRIS’s visual trace and logging tools. The architecture relies on certain key elements:

  1. Business Hosts: These are the core building blocks—Services, Processes, and Operations—that pass persistable messages between one another.
  2. Adapters: Inbound and outbound adapters manage the interaction with the external world, handling the specific protocols needed to receive and send data.
  3. Callbacks: The engine uses specific callback methods to pass messages between hosts, either synchronously or asynchronously. These callbacks follow strict signatures and return a Status object to ensure execution integrity.
  4. Configuration Helpers: Objects such as Properties and Parameters expose settings to the Production Configuration UI, allowing users to easily instantiate, configure, and save the state of these components.

Workflow using pyprod

This is essentially a 3 step process.

  1. Write your production components in a regular Python script. In that script, you import the required base classes from intersystems_pyprod and define your own components by subclassing them, just as you would with any other Python library.
  2. Load them into InterSystems IRIS by running the intersystems_pyprod (same name as the library) command from the terminal and passing it the path to your Python script. This step links the Python classes with IRIS so that they appear as production components and can be configured and wired together using the standard Production Configuration UI. 
  3. Create the production using the Production Configuration page and start the Production

NOTE: If you create all your components with all their Properties hardcoded within the python script, you only need to add them to the production and start the Production. 

You can connect pyprod to your IRIS instance by doing a one time setup


Simple Example

In this example, we demonstrate a synchronous message flow where a request originates from a Service, moves through a Process, and is forwarded to an Operation. The resulting response then travels the same path in reverse, passing from the Operation back through the Process to the Service. Additionally, we showcase how to utilize the IRISLog utility to write custom log entries.

Step 1

Create your Production components using pyprod in the file HelloWorld.py

Here are some key parts of the code

  • Package Naming: We define iris_package_name, which prefixes all classes as they appear on the Production Configuration page (If omitted, the script name is used as the default prefix).
  • Persistable Messages: We define MyRequest and MyResponse. These are the essential data structures for communication, as only persistable objects can be passed between Services, Processes, and Operations.
  • The Inbound Adapter: Our adapter passes a string to the Service using the business_host_process_input method.
  • The Business Service: Implemented with the help of OnProcessInput callback.
    • MyService receives data from the adapter and converts it into a MyRequest message
    • We use the ADAPTER IRISParameter to link the Inbound Adapter to the Service. Note that this attribute must be named ADAPTER in all caps to align with IRIS conventions.
    • We define a target IRISProperty, which allows users to select the destination component directly via the Configuration UI.
  • The Business Process: Implemented with the help of OnRequest callback.
  • The Business Operation: Implemented with the help of OnMessage callback. (You can also define a MessageMap)
  • Logic & Callbacks: Finally, the hosts implement their core logic within standard callbacks like OnProcessInput and OnRequest, routing messages using the SendRequestSync method.

You can read more about each of these parts on the pyprod API Reference page and also using the Quick Start Guide.

import time

from intersystems_pyprod import (
    InboundAdapter,BusinessService, BusinessProcess, 
    BusinessOperation, OutboundAdapter, JsonSerialize, 
    IRISProperty, IRISParameter, IRISLog, Status)

iris_package_name = "helloworld"
class MyRequest(JsonSerialize):
    content: str

class MyResponse(JsonSerialize):
    content: str

class MyInAdapter(InboundAdapter):
    def OnTask(self):
        time.sleep(0.5)
        self.business_host_process_input("request message")
        return Status.OK()

class MyService(BusinessService):
    ADAPTER = IRISParameter("helloworld.MyInAdapter")
    target = IRISProperty(settings="Target")
    def OnProcessInput(self, input):
        persistent_message = MyRequest(input)
        status, response = self.SendRequestSync(self.target, persistent_message)
        IRISLog.Info(response.content)
        return status

class MyProcess(BusinessProcess):
    target = IRISProperty(settings="Target")
    def on_request(self, input):
        status, response = self.SendRequestSync(self.target,input)
        return status, response


class MyOperation(BusinessOperation):
    ADAPTER = IRISParameter("helloworld.MyOutAdapter")
    def OnMessage(self, input):
        status = self.ADAPTER.custom_method(input)
        response = MyResponse("response message")
        return status, response


class MyOutAdapter(OutboundAdapter):
    def custom_method(self, input):
        IRISLog.Info(input.content)
        return Status.OK()

 

Step 2

Once your code is ready, load the components to IRIS.

$ intersystems_pyprod /full/path/to/HelloWorld.py

    Loading MyRequest to IRIS...
    ...
    Load finished successfully.
    
    Loading MyResponse to IRIS...
    ...
    Load finished successfully.
    ...
    

Step 3

Add each host to the Production using the Production Configuration page.

The image below shows MyService and its target property being configured through the UI. Follow the same process to add MyProcess and MyOperation. Once the setup is complete, simply start the production to see your messages in motion.


Final Thoughts

By combining the flexibility of the Python ecosystem with the industrial-grade reliability of InterSystems IRIS, pyprod offers a modern path for building interoperability solutions. Whether you are developing entirely new "Pure Python" productions or enhancing existing ObjectScript infrastructures with specialized Python libraries, pyprod ensures your components remain fully integrated, observable, and easy to configure. We look forward to seeing what you build!


Quick Links

GitHub repository  

PyPi Package

Support the Project: If you find this library useful, please consider giving us a ⭐ on GitHub and suggesting enhancements. It helps the project grow and makes it easier for other developers in the InterSystems community to discover it!
ディスカッション (0)1
続けるにはログインするか新規登録を行ってください