There are numerous excellent tools available for testing your REST APIs, especially when they are live. Postman, various web browser extensions, and even custom ObjectScript written with %Net.HttpRequest objects can get the job done. However, it is often difficult to test just the REST API without inadvertently involving the authentication scheme, the web application configuration, or even network connectivity. Those are a lot of hoops to jump through just to test the code within your dispatch class. The good news is that if we take our time to understand the inner workings of the %CSP.REST class, we will find an alternative option suited for testing only the contents of the dispatch class. We can set up the request and response objects to invoke the methods directly.
Understanding %request and %response
If you have ever worked with REST APIs in InterSystems, you have probably encountered the two objects that drive the entire process: %request and %response. To grasp their utility, we must recall our ObjectScript fundamentals regarding "percent variables." Typically, a variable in ObjectScript can only be accessed directly within the process that defines it. However, a variable prefixed with a percent sign is public within that process. It means that all methods within the same process can access it. That is why by defining those variables ourselves, we can trigger our REST API methods exactly as they would run when receiving a genuine incoming request in the API.
Consider the following basic %CSP.REST class. It features two routes and two corresponding methods. The first route accepts a request containing a date of birth in ODBC format and returns the age in days. The second one accepts a name as a URL parameter, makes sure the request method is correct, and simply echoes a hello message.
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."
}
}
}
If we attempt to call these methods from a terminal session, they will fail because %request is undefined.
USER>w $SYSTEM.Status.GetErrorText(##class(User.REST).CalcAge())
ERROR #5002: ObjectScript error: <INVALID OREF>CalcAge+2^User.REST.1
Normally, the %CSP.REST class creates this object from the received HTTP request as an instance of %CSP.Request. Likewise, the %response object is developed as an instance of %CSP.Response. Instead of sending an HTTP request, we will define these objects ourselves.
USER>set %request = ##class(%CSP.Request).%New()
USER>set %request.Content = ##class(%CSP.CharacterStream).%New()
USER>do %request.Content.Write("{""dob"":""1900-01-01""}")
USER>set %response = ##class(%CSP.Response).%New()
USER>do ##class(User.REST).CalcAge()
{"age_in_days":45990}
In these few lines, we design the request and initialize its Content property to a %CSP.CharacterStream. This step is vital as otherwise that property will remain undefined and unable to be written to. Another valid alternative would be %CSP.BinaryStream. We populate the content stream with some JSON and initialize the %response to be a %CSP.Response without needing to do anything to it. It simply needs to be defined for the method to run. This time, when we call our class method, we can see the expected output. We could also issue a zwrite %response command to inspect the response content type or HTTP status code for assistance with troubleshooting any issues that might arise.
Testing the output of our class method takes only five lines. Tt is obviously an intentionally simplified example. However, it is far more efficient than navigating the System Management Portal to configure an application and creating the necessary HTTP requests to authenticate and call this method! It also allows us to verify the method in total isolation. When we send a request through the typical tools we employ to test REST APIs, we verify the entire stack: network connectivity, application configuration, authentication mechanism, request dispatching, and the method - all at once. The approach described above, on the other hand, allows us to test exclusively the method defined in the API, making it easier to find the problem during debugging.
Similarly, we can test the second method by setting the appropriate %request object and then calling it. In this case, we will pass the name the same way we would for any other class method.
USER>set %request = ##class(%CSP.Request).%New()
USER>set %request.Method = “GET”
USER>set %response = ##class(%CSP.Response).%New()
USER>do ##class(User.REST).EchoName(“Dave”)
Hello, Dave
Dispatching Requests
We can take this a step further and test the dispatching as well. You may have noticed that I manually checked the CSP request’s HTTP method in the EchoName example. I did it because direct method calls bypass the routing rules, which typically prevents incorrect mapping.
If we decide to continue with our terminal session with the %request we have defined, we can determine its HTTP method as follows:
USER>set %request.Method = “GET”
To test the dispatch, we should simply call the class DispatchRequest method, and if it is successful, we will see the output from our method again:
USER>do ##class(User.REST).DispatchRequest(“/calc/age/Dave”,%request.Method)
Hello, Dave
With the %request object properly configured, including the HTTP method, we can validate our routes. We can perform a similar test for the age calculation endpoint by setting up our %request again as we did before and employing the POST method
USER>set %request = ##class(%CSP.Request).%New()
USER>set %request.Content = ##class(%CSP.CharacterStream).%New()
USER>do %request.Content.Write("{""dob"":""1900-01-01""}")
USER>set %request.Method = “POST”
USER>set %response = ##class(%CSP.Response).%New()
USER>do ##class(User.REST).CalcAge()
USER>do ##class(User.REST).DispatchRequest(“/calc/age”,%request.Method)
{"age_in_days":45990}
Note that we have defined the URL argument for the DispatchRequest method to be only the part of the URL located in the route node defined in the dispatch class’s XData. his is all we need to test the dispatching! This allows us to test things independently without needing to know the final deployment URL or its web application's configuration. Thus, we have been able to test everything that we typically define within a %CSP.REST class without having to worry about the usual external concerns surrounding it.
By the way, I have been starting the sessions by setting %request to a new instance of %CSP.Request. However, if you plan to do multiple tests in a row, you could also utilize the %request.Reset() method to start over with your request object. The same is true for the %response object.
Since your request might be more complicated than this one, you should familiarize yourself with the properties and methods of the %CSP.Request class. For instance, it is quite common for your API to check the content type of the request and ensure that it is whatever the API expects. In this case, you can set %request.ContentType to be application/json or whatever else is appropriate for your usage. You can configure cookies, mime data, and request data. You can also set the Secure property to simulate whether the request used HTTPS or not.
A Couple of Caveats
There are two primary considerations to keep in mind when testing this way. First, if our API returns a file, the terminal output can become a bit unwieldy. In such instances, you should redirect the output to a file. We can accomplish that with the following commands:
USER>set %request = ##class(%CSP.Request).%New()
USER>set %request.Method = "GET"
USER>set io = "C:\Users\DavidHockenbroch\Desktop\output.txt"
USER>open io:"NW":10
USER>write $TEST
1
USER>use io do ##class(User.REST).DispatchRequest("/echoname/Dave",%request.Method)
USER>close io
I now have a text file on my desktop that says “Hello, Dave.” Using N and W in the open command will create the file if it does not exist yet, and open it for writing. Note that we check $TEST. If that variable is 0 when we check it, there has been a problem opening the file as a device for writing. This could indicate a file permission issue, or the file may already be opened and locked by another process. As a matter of good practice, we should always remember to close the device.
The second catch is that if we plan on setting a special session event class in the web application configuration, and those custom events will impact the way our REST methods work, it can cause problems since we are bypassing those methods. We will have to call them as class methods manually in the terminal.
Onward!
Now that we have established some means for testing our API without any extra baggage, we are ready to move to our true goal: unit testing. Stay tuned for the next article!