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.
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
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!