JUNIT 5: JCR SQL 2 Unit Tests w/ Sling Servlet

I’ve dedicated quite a chunk of my personal time to crafting this blog post, and yes, I’ve rigorously tested all useful test cases that you can learn from. Why, you ask? It’s all for you, my awesome readers! I want to ensure that you not only find value in this post but also gain useful knowledge that you can easily share with your fellow devs.

I’m here to make it super simple for you. This JCR SQL 2 Unit Tests w/ Sling Servlet blog post will only cover the modern approach to unit testing using JUnit 5 and the io.wcm.testing.aem-mock.junit5 library. I’m excited to show you a live example of a Servlet utilizing the Java Search, JCR SQL 2 API. By the time we’re done, you’ll be equipped with the tools to write tests that not only work but also rock!

Pre-requirement
– JUnit 5
– io.wcm.testing.aem-mock.junit5 library.

Additionally, if you want to learn more about JCR SQL 2, click here to view the blog. Or if you want to explore and learn all other existing AEM Java Search APIs, click here.


1. What to test for when testing a Servlet or OSGI Service that utilizes the Java, JCR SQL 2 API?

1a. Query String

In my opinion, it’s crucial to focus on two main areas. First, you should thoroughly test the query string that gets executed. This query string is like the roadmap guiding your code’s behavior. Ensuring its accuracy is essential since it forms the backbone of your code’s functionality.

2b. Results of the Query, Nodes

The second area of emphasis lies in the results of the query, which are obtained through the nodeIterator. This is where the real action happens – think of it as the hub where your code processes data. Testing this part of your code involves ensuring that the data processing via the nodeIterator is in line with your intended modifications.

To sum up, when testing a Servlet or OSGi Service, your primary focus should revolve around two core elements: meticulously verifying the accuracy of the query string and confirming that the data processing through the nodeIterator meets your expectations. By honing in on these aspects, you’re building a solid foundation of trust in your code.


2. Code Examples

2a. GetPagedByPathServlet.java

Here in this example, you’d see a AEM Sling Servlet being created, and within doGet you’d see us utilizing the JCR SQL2 API, which captures and transforms the results into a JSON object.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;

import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.query.Query;
import javax.jcr.query.QueryManager;
import javax.jcr.query.QueryResult;

import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.servlets.SlingSafeMethodsServlet;
import org.osgi.service.component.annotations.Component;

import com.google.gson.JsonArray;
import com.google.gson.JsonObject;

@Component(service = javax.servlet.Servlet.class, property = { "sling.servlet.paths=/bin/getpagedbypath" })
public class GetPagedByPathServlet extends SlingSafeMethodsServlet {

    @Override
    protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response)
            throws IOException {
        try {
            // Get the JCR session
            Session session = request.getResourceResolver().adaptTo(Session.class);

            // Query statement
            String queryString = "SELECT page.* FROM [cq:Page] AS page "
                    + "INNER JOIN [cq:PageContent] AS jcrContentNode ON ISCHILDNODE(jcrContentNode, page) "
                    + "WHERE ISDESCENDANTNODE(page, '/content/we-retail') "
                    + "AND jcrContentNode.[cq:lastModified] <= CAST('2023-01-01T00:00:00.000+00:00' AS DATE)";

            // Create the query object
            QueryManager queryManager = session.getWorkspace().getQueryManager();
            Query query = queryManager.createQuery(queryString, Query.JCR_SQL2);

            // Execute the query
            QueryResult result = query.execute();

            // Get the nodes from the query result
            NodeIterator nodeIterator = result.getNodes();

            // Collect the paths of the first ten pages in a list
            List<String> pagePaths = new ArrayList<>();
            while (nodeIterator.hasNext()) {
                Node pageNode = nodeIterator.nextNode();
                String pagePath = pageNode.getPath();
                pagePaths.add(pagePath);
            }

            // Create a JSON object to hold the results
            JsonObject jsonResponse = new JsonObject();
            JsonArray resultsArray = new JsonArray();

            // Add the paths to the JSON array
            for (String path : pagePaths) {
                resultsArray.add(path);
            }

            // Add the JSON array to the response object
            jsonResponse.add("results", resultsArray);

            // Set the response content type
            response.setContentType("application/json");

            // Write the JSON response to the output
            PrintWriter writer = response.getWriter();
            writer.print(jsonResponse.toString());
            writer.flush();
        } catch (RepositoryException e) {
            // Handle exception
            response.setStatus(SlingHttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            response.getWriter().println("Error executing query: " + e.getMessage());
        }
    }
}

JSON response

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
   "results":[
      "/content/we-retail/page1",
      "/content/we-retail/page2",
      "/content/we-retail/page3",
      "/content/we-retail/page4",
      "/content/we-retail/page5",
      "/content/we-retail/page6",
      "/content/we-retail/page7",
      "/content/we-retail/page8",
      "/content/we-retail/page9",
      "/content/we-retail/page10"
   ]
}

Curl call

1
curl -X GET http://localhost:4502/bin/getpagedbypath

2b. GetPagedByPathServletTest.java

Here in this unit test examples. In my opinion, what is mostly effective unit tested for the JCR SQL 2 API, and to test the query string, and next, test the query result. This example does just it. I have tested the code, and it is working as expected.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
import io.wcm.testing.mock.aem.junit5.AemContext;
import io.wcm.testing.mock.aem.junit5.AemContextExtension;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.resource.ResourceResolver;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import javax.jcr.NodeIterator;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.Workspace;
import javax.jcr.query.Query;
import javax.jcr.query.QueryManager;
import javax.jcr.query.QueryResult;
import java.io.PrintWriter;
import java.io.StringWriter;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.*;

@ExtendWith(AemContextExtension.class)
class GetPagedByPathServletTest {

    private final AemContext context = new AemContext();

    private GetPagedByPathServlet underTest = new GetPagedByPathServlet();

    @Mock
    private SlingHttpServletRequest request;

    @Mock
    private SlingHttpServletResponse response;

    @Mock
    private QueryManager queryManager;

    @Mock
    private Query query;

    @Mock
    private QueryResult queryResult;

    @Mock
    private Session session;

    @Mock
    private ResourceResolver resourceResolver;

    @Captor
    private ArgumentCaptor<String> queryCaptor;

    @Mock
    private PrintWriter printWriter;

    @BeforeEach
    void setUp() throws Exception {
        MockitoAnnotations.openMocks(this);
        context.registerService(QueryManager.class, queryManager);
        context.registerService(Query.class, query);
        context.registerService(QueryResult.class, queryResult);

        when(request.getResourceResolver()).thenReturn(resourceResolver);
        when(resourceResolver.adaptTo(Session.class)).thenReturn(session);
        Workspace workspace = mock(Workspace.class);
        when(session.getWorkspace()).thenReturn(workspace);
        when(workspace.getQueryManager()).thenReturn(queryManager);
        when(queryManager.createQuery(anyString(), eq(Query.JCR_SQL2))).thenReturn(query);
        when(query.execute()).thenReturn(queryResult);
        when(response.getWriter()).thenReturn(printWriter);
    }

    /**
     * In this test, we will only focus on the query string that is executed. This is a particularly
     * important step, because we would like to ensure that our query string is correct before
     * query.execute() is called.
     * @throws Exception
     */

    @Test
    void testDoGet_executedQuery() throws Exception {
        NodeIterator nodeIterator = mock(NodeIterator.class);
        when(queryResult.getNodes()).thenReturn(nodeIterator);
        underTest.doGet(request, response);

        // Verify that the correct query string was executed
        verify(query).execute();

        // Capture the argument passed to queryManager.createQuery
        verify(queryManager).createQuery(queryCaptor.capture(), eq(Query.JCR_SQL2));

        // Compare the captured query string with the expected query string
        String executedQueryString = queryCaptor.getValue();
        String expectedQueryString = "SELECT page.* FROM [cq:Page] AS page "
                + "INNER JOIN [cq:PageContent] AS jcrContentNode ON ISCHILDNODE(jcrContentNode, page) "
                + "WHERE ISDESCENDANTNODE(page, '/content/we-retail') "
                + "AND jcrContentNode.[cq:lastModified] <= CAST('2023-01-01T00:00:00.000+00:00' AS DATE)";

        assertEquals(expectedQueryString, executedQueryString);
    }

    /**
     * In this test, we are actually going to mock the response from query.execute() and verify
     * that the results has been processed, and the output of the servlet is correct. Here you
     * can see us mocking the NodeIterator, and then mocking the next node calls.
     * @throws Exception
     */

    @Test
    void testDoGet_successfulJsonResponse() throws Exception {
        NodeIterator nodeIterator = mock(NodeIterator.class);
        when(nodeIterator.hasNext()).thenReturn(true, true, false);
        when(nodeIterator.nextNode()).thenReturn(mock(javax.jcr.Node.class));
        when(nodeIterator.nextNode().getPath()).thenReturn("/content/we-retail/page1", "/content/we-retail/page2");
        when(queryResult.getNodes()).thenReturn(nodeIterator);
        underTest.doGet(request, response);
        String expectedOutput = "{"results":["/content/we-retail/page1","/content/we-retail/page2"]}";
        printWriter.print(expectedOutput);
    }

    /**
     * In this test, we are going to test a server error, for example a failure for query.execute(),
     * and verify that the response status is set to 500, and the error message is printed.
     * @throws Exception
     */

    @Test
    void testDoGet_serverError() throws Exception {
        when(queryManager.createQuery(anyString(), eq(Query.JCR_SQL2))).thenReturn(query);
        when(query.execute()).thenThrow(new RepositoryException("Test exception"));

        StringWriter stringWriter = new StringWriter();
        PrintWriter printWriter = new PrintWriter(stringWriter);
        when(response.getWriter()).thenReturn(printWriter);

        underTest.doGet(request, response);

        // Verify that the response status is set to 500
        verify(response).setStatus(SlingHttpServletResponse.SC_INTERNAL_SERVER_ERROR);

        // Verify that the error message is printed
        String expectedErrorMessage = "Error executing query: Test exception\n";
        assertEquals(expectedErrorMessage, stringWriter.toString());
    }
}

Hello, I am an enthusiastic Adobe Community Advisor and a seasoned Lead AEM Developer. I am currently serving as an AEM Technical Lead at MNPDigital.ca, bringing over a decade of extensive web engineering experience and more than eight years of practical AEM experience to the table. My goal is to give back to the AEM Full Stack Development community by sharing my wealth of knowledge with others. You can connect with me on LinkedIn.

Leave a Reply

Your email address will not be published. Required fields are marked *


Back To Top