Unit Testing Hands on - For Sling Servlet


Unit Testing for Sling Servlet
This post is about creating Unit Test class for Sling Servlet, another commonly used Java class as part of an AEM application. 
In Sling servlets, we have 
  • SlingSafeMethodsServlet - read only servlet supporting GET (doGet)
  • SlingAllMethodsServlet - Supports POST, PUT and DELETE (doPost/doPut/doDelete)
In either case, we have request and response objects using which desired code logic is written in Servlet. 
For Unit testing, we have Mock sling request and sling response from Sling Mocks (org.apache.sling.testing.mock.sling.servlet.MockSlingHttpServletRequest  and org.apache.sling.testing.mock.sling.servlet.MockSlingHttpServletResponse respectively)

Writing unit test for Servlet involves the following
  • Instantiate Servlet to be tested.
  • Get mock request and response from AemContext
  • Prepare the mock request object (per the code logic before calling doGet/doPost)
  • Call doGet/doPost method of Servlet to be tested
  • Assert the Servlet response (per the code logic)
Instantiate Servlet to be tested.
Considering the Servlet to be tested is UnitTestServlet.java
  • UnitTestServlet unitTestServletObj = new UnitTestServlet();
Get request and response from AemContext:
From AemContext object, we can create request and response which is MockSlingHttpServletRequest and MockSlingHttpServletResponse respectively as explained above. 
  • MockSlingHttpServletRequest mockSlingRequest = aemContext.request();
  • MockSlingHttpServletResponse mockSlingResponse = aemContext.response();
Prepare mock request object:
Consider a scenario where we have a Servlet to retrieve values from request and perform logic based on that. 
Lets say, parameter named "siteName" is retrieved from request via request.getParameter("siteName") where siteName is being passed from HTML form or ajax request/any related.
Now for unit testing, we need to explicitly set the parameter to the mock request. So when Servlet method is tested and when above line executes, we will have a value for the "siteName".

Prepare Map object and set it to mock sling request as below
  • private Map<String, Object> parameterMap = new HashMap<String, Object>();
  • parameterMap.put("siteName", "aemlearnings");
  • mockSlingRequest.setParameterMap(parameterMap);
Similarly, we can gain access to other methods available in MockSlingRequest to prepare mock sling request (mockSlingRequest) per the code logic for testing. (mockSlingRequest.setResource(), mockSlingRequest.addHeader and so on)

Call doGet/doPost method of Servlet to be tested:
Once when the mock sling request object is prepared, we can call the respective method using the servlet object that we instantiated per first step. 
  • unitTestServletObj.doGet(mockSlingRequest, mockSlingResponse);
When this line gets executed, code logic in actual doGet method will be executed. If there is anything that is missed to set up in Test class file, then this line might throw "NullPointerException"
Example:
Let say code logic in doGet makes use of an OSGI service + if that OSGI service is not registered in Test class file/ in mock sense, if we don't define any dummy implementation for any of the lines of code, then the respective object will be null and hence exception in above line of execution (or if exception is handled, it will ultimately result in an error related to Assertion)

Assert the Servlet response:
Based on the code logic related to response object, we can assert accordingly.
Example to assert the status code/check the content type set/anything written to response object.
  • /* Checking content Type */
  • assertEquals("application/json", mockSlingResponse.getContentType());
  • /* Checking response status code */
  • assertEquals(200, mockSlingResponse.getStatus());
  • /* Checking output of servlet */
  • assertTrue(mockSlingResponse.getOutputAsString().contains("Response String from Servlet")); [where the actual line of code for this is something like - resp.getWriter().write("Response String from Servlet=" + responseStr);]
Code on GitHub:
Full Sample Servlet and Unit Test class for the same is available in GitHub
As such it doesn't refine to a specific functionality but to illustrate the below
  • Reference an OSGI Service
  • Call to a dummy REST API (available from POSTMAN) and writes the API response to Servlet response object. 
Reference an OSGI service: 
We have an option to register/inject OSGI service with the help of AemContext.
Considering an OSGI service (with config) is referenced in a Servlet, then in Test class file, we need to register that service using AemContext - We have few related methods from AemContext for the same. Based on the nature of OSGI service referenced, we can use the respective method accordingly.

Full list of methods related to this is available under "Methods from OSGIContextImpl" section in API Doc

Prepare the config properties using a Map object
Use registerInjectActivateService method from AemContext
  • private SampleOSGIService mockSampleOSGIService; // OSGI service 
  • private Map<String, String> configProps = new HashMap<String, String>();
  • configProps.put("apiEndpoint", "https://postman-echo.com");
  • configProps.put("apiKey", "XYZ");
  • configProps.put("siteName", "aemlearnings");
  • mockSampleOSGIService = aemContext.registerInjectActivateService(new SampleOSGIServiceImpl(), configProps);
Other use cases part of a Servlet:
Logic related to accessing resource from request/performing any iteration/related actions on AEM APIs like Page or Asset or Tag or any related/Query Builder based logic to get the result based on some condition via predicates Map and so on.
We can mock the respective API if it is not directly available from AemContext -> provide dummy implementation and hence assert accordingly. 

Comments

Popular posts from this blog

Embedding Third party dependency/OSGi bundle in AEM application hosted in AEMasCS

OSGI Factory Configuration implementation

Creation of Template Types for Editable templates