Implementing unit tests in an AEM project ensures reliable code, early bug detection, and efficient development. However, developers may skip writing unit tests or face challenges in improving them. This article proposes an approach to applying unit tests, particularly for key components used AEM project like Sling models and Sling servlets, during AEM project development.
Install dependencies in the all/pom.xml and core/pom.xml files within the AEM project structure. Begin by visiting https://mvnrepository.com to gather all the test dependencies needed for the AEM project.
As you can see in the screenshot above, we are searching for the junit-jupiter-api dependency on the Maven tab and putting it into your pom.xml file. In reality, you need more than just this dependency.
» all/pom.xml
<embeddeds>
<embedded>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<target>/apps/flagtick-packages/application/install</target>
</embedded>
<embedded>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<target>/apps/flagtick-packages/application/install</target>
</embedded>
<embedded>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<target>/apps/flagtick-packages/application/install</target>
</embedded>
<embedded>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<target>/apps/flagtick-packages/application/install</target>
</embedded>
<embedded>
<groupId>junit-addons</groupId>
<artifactId>junit-addons</artifactId>
<target>/apps/flagtick-packages/application/install</target>
</embedded>
</<embeddeds>
...
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>{version}</version>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>{version}</version>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<version>{version}</version>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>{version}</version>
</dependency>
<dependency>
<groupId>junit-addons</groupId>
<artifactId>junit-addons</artifactId>
<version>{version}</version>
</dependency>
</dependencies>
Note: In reality, you don't have to implement dependencies in the pom.xml file for all modules in AEM if there are issues with packages not being found. Therefore, the implementation is established in specific modules, such as the core module, for example.
» core/pom.xml
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit-addons</groupId>
<artifactId>junit-addons</artifactId>
<scope>test</scope>
</dependency>
The next step is to set up the AEM project using the Maven archetype. Here is an example of the project folder structure, where you can find all essential modules. Then, we will focus on the core module to conduct writing unit tests for key components such as Sling Models, Sling Servlets,...
+--- flagtick
+--- all
+--- core
+--- ui.apps
+--- ui.apps.structure
+--- ui.config
+--- ui.content
+--- ui.frontend
+--- ui.tests
+--- dispatcher (flagtick.dispatcher.cloud)
+--- ...
After successfully configuring the pom.xml file in the core module, execute mvn clean install to fetch all dependencies to your AEM project.
First off, let us show how to set up Sling Model unit test. Imagine we have got Login page with Login component. The goal of our test is to check few scenarios, like:
You can refer to the screenshot below to understand how we have structured the example.
Take a moment to read here and understand how we assign value for property redirectionPath in the Sling Model without relying on the touch UI dialog. We achieve this by leveraging the use of AEM Page.
MockedStatic works with JUnit 5's @ExtendWith(MockitoExtension.class) or JUnit 4's @RunWith(MockitoJUnitRunner.class). Create an instance for the class with static methods you want to mock.
@ExtendWith({
AemContextExtension.class,
MockitoExtension.class})
class LoginModelImplTest {
private static MockedStatic<PageUtils> pageUtilsMockedStatic;
@BeforeAll
public static void init() {
pageUtilsMockedStatic = Mockito.mockStatic(PageUtils.class);
}
@AfterAll
public static void close() {
pageUtilsMockedStatic.close();
}
// TODO
}
In the declaration of the LoginModelImpl class, the usage of @AemObject private Page currentPage; indicates that during unit testing, it is necessary to define or mock the @AemObject dependency to prevent encountering errors when the class is scanned by the testing framework.
@Mock
private XSSAPI genericXxsApi;
@BeforeEach
public void setUp() {
// Register injector for @AemObject fields
context.registerService(XSSAPI.class, genericXxsApi);
context.registerInjectActivateService(new AemObjectInjector());
}
Next part, we will register Sling Models and loads content into the AEM repository to create a controlled environment for testing specific scenarios related to the LoginModel and the content structure of the AEM instance.
import static com.flagtick.core.utils.TestConstants.RESOURCE_MODELS_ROOT_PATH;
@BeforeEach
public void setUp() {
{
// Register models
context.addModelsForClasses(LoginModel.class);
// Load content
context.load().json(RESOURCE_MODELS_ROOT_PATH + "/en.json", "/content/flagtick/us/en");
}
Note: Refer to this link for a guide on creating both the en.json file and then, we can retrieve the children page of the parent en page using context.resourceResolver().resolve(...) and validate the template type on its child pages, such as the dashboard page.
» TestConstants.java
package com.flagtick.core.utils;
import com.day.cq.commons.jcr.JcrConstants;
public class TestConstants {
public static final String RESOURCE_MODELS_ROOT_PATH = "/models";
}
When retrieving the full content of an AEM page, it allows you to view the page structure in JSON format. This structure includes all the components that can be dragged and dropped onto each page.
{
"jcr:primaryType": "cq:Page",
"jcr:createdBy": "admin",
"jcr:created": "Fri Oct 20 2023 15:53:01 GMT+0700",
"jcr:content": {},
"dashboard": {},
"login": {
"jcr:primaryType": "cq:Page",
"jcr:content": {
"jcr:primaryType": "cq:PageContent",
"cq:template": "/conf/flagtick/settings/wcm/templates/login-template",
"cq:lastModifiedBy": "[email protected]",
"root": {
"jcr:primaryType": "nt:unstructured",
"layout": "responsiveGrid",
"sling:resourceType": "flagtick/components/content/container",
"container": {
"jcr:primaryType": "nt:unstructured",
"layout": "responsiveGrid",
"sling:resourceType": "flagtick/components/content/container",
"title": {
"jcr:primaryType": "nt:unstructured",
"sling:resourceType": "flagtick/components/content/title"
},
"container": {
"jcr:primaryType": "nt:unstructured",
"layout": "responsiveGrid",
"sling:resourceType": "flagtick/components/content/container",
"login": {
"jcr:primaryType": "nt:unstructured",
"jcr:createdBy": "admin",
"loginIcon": "/content/dam/flagtick/logos/Logo.svg",
"headline": "Flagtick Portal Login",
"sling:resourceType": "flagtick/components/content/account/login"
}
}
}
}
}
}
}
If you look at 'sling:resourceType': 'flagtick/components/content/account/login', you can declare two parameters: loginPagePath and loginPath. The loginPagePath is the path to the login page, and loginPath is the path to the login component used within the login page.
import static com.flagtick.core.utils.TestConstants.RESOURCE_MODELS_ROOT_PATH;
private final String loginPagePath = "/content/flagtick/us/en/login";
private final String loginPath = loginPagePath + "/jcr:content/root/container/login";
@BeforeEach
public void setUp() {
{
// Load content
context.load().json(RESOURCE_MODELS_ROOT_PATH + "/login.json", loginPagePath);
}
Write unit test for the LoginModel class to ensure the successful creation of the model and verify that specific properties of the model match the expected values.
@Test
void testModelInitialization() {
// Set component's page
context.currentPage(loginPagePath);
// Set component's resource
context.currentResource(loginPath);
LoginModel model = context.getService(ModelFactory.class).createModel(context.request(), LoginModel.class);
// Verify
assertNotNull(model);
assertEquals("/content/dam/flagtick/Logo.svg", model.getLoginIcon());
assertEquals("Flagtick Portal Login", model.getHeadline());
}
First and foremost, we will determine the root path for the experience fragment when applying it to each project in the AEM instance. Let us observe this in CRXDE Lite. For example:
As such, we only have to declare navigationPath variable and no navigationPagePath variable. This is because the experience fragment will be set statically in the template, and then all pages created will inherit it.
» NavigationModelImplTest.java
@ExtendWith({
AemContextExtension.class,
MockitoExtension.class})
class NavigationModelImplTest {
private final String navigationPath = EXPERIENCE_FRAGMENT_ROOT_PATH + "/header" + MASTER_ROOT_PATH + "/navigation";
@BeforeEach
public void setUp() {
// Register models
context.addModelsForClasses(NavigationModelImpl.class);
// Load content
context.load().json(RESOURCE_MODELS_ROOT_PATH + "/navigation.json", navigationPath);
// Set component's resource
context.currentResource(navigationPath);
model = context.getService(ModelFactory.class).createModel(context.request(), NavigationModel.class);
}
}
» TestConstants.java
public class TestConstants {
public static final String EXPERIENCE_FRAGMENT_ROOT_PATH = "/content/experience-fragments/flagtick/us/en/site";
public static final String MASTER_ROOT_PATH = "/master/jcr:content/root";
}
Based on navigationPath variable, add either .-1.json or .infinity.json at the end. Hence, access http://localhost:4502/content/experience-fragments/flagtick/us/en/site/header/master/jcr:content/root/navigation.html.-1.json and you will retrieve all information for AEM navigation component. Normally, the multifield is one of the reasons why we prefer using Sling Models instead of default properties to retrieve field values from the Touch UI dialog in AEM components.
» _cq_dialog > .content.xml
<navItems
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/multifield"
composite="{Boolean}true"
fieldLabel="Navigation Menu Items">
<field
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/container"
name="./flagtick/navItems">
<items jcr:primaryType="nt:unstructured">
<navItemIcon
jcr:primaryType="nt:unstructured"
sling:resourceType="cq/gui/components/authoring/dialog/fileupload"
allowUpload="{Boolean}false"
autoStart="{Boolean}false"
class="cq-droptarget"
fieldLabel="Nav Item Icon"
fileNameParameter="./icon"
fileReferenceParameter="./navItemIcon"
mimeTypes="[image]"
multiple="{Boolean}false"
name="./file"
title="Nav Item Icon"
useHTML5="{Boolean}true"/>
<navItemText
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
fieldLabel="Nav Title"
name="./navItemText"/>
<navItemSubtext
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
fieldLabel="Nav Subtitle"
name="./navItemSubtext"/>
<navItemURL
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/pathbrowser"
fieldLabel="Nav Item URL"
name="./navItemURL"
rootPath="/content/flagtick/us/en"/>
<target
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/select"
fieldLabel="Link Behavior"
required="{Boolean}true"
name="./opensIn">
<items jcr:primaryType="nt:unstructured">
<sameTab
jcr:primaryType="nt:unstructured"
text="Same Tab"
value="_self"/>
<newTab
jcr:primaryType="nt:unstructured"
text="New Tab"
value="_blank"/>
</items>
</target>
</items>
</field>
</navItems>
Here is the way we retrieve a list of child resources located at the path ./flagtick/navItems relative to the current resource.
@ChildResource(name = "./flagtick/navItems")
private List<NavigationItem> navItems;
Thus, the idea is to design a unit test to validate this case. An example is provided below for your reference:
@Test
void testModelInitialization() {
Resource navItemsRes = context.resourceResolver().getResource(navigationPath + "/flagtick/navItems");
List<NavigationItem> items = new ArrayList<>();
Iterator<Resource> iterator = navItemsRes.listChildren();
while (iterator.hasNext()) {
Resource itemRes = iterator.next();
NavigationItem navItem = itemRes.adaptTo(NavigationItem.class);
items.add(navItem);
}
List<NavigationItem> modelitems = model.getNavItems();
assertEquals(items.size(), modelitems.size());
}
Assume we have Login component and Login page. Within the Login component, we establish REST API call to servlet named LoginServlet. Here are the details of the servlet design diagram:
Here is an example where we implement on LoginServlet.java to retrieve information from the user after login. We validate the session using an AccessToken and leverage the UserID to retrieve essential information for the user in the accessed system.
» LoginServlet.java
@Component(immediate = true, service = Servlet.class,
property = {
SERVICE_DESCRIPTION + LoginServlet.SERVLET_SERVICE_DESCRIPTION,
SLING_SERVLET_RESOURCE_TYPES + LoginServlet.RESOURCE_TYPE,
SLING_SERVLET_METHODS + "=" + HttpConstants.METHOD_GET,
SLING_SERVLET_SELECTORS + LoginServlet.SELECTOR,
SLING_SERVLET_EXTENSIONS + "=" + Constants.EXTENSION_JSON
})
public class LoginServlet extends SlingSafeMethodsServlet {
public static final String SERVLET_SERVICE_DESCRIPTION = "=FLAGTICK - LOGIN TO GET USER INFORMATION";
public static final String RESOURCE_TYPE = "=flagtick/components/content/account/login";
public static final String SELECTOR = "=user-info";
private static final long serialVersionUID = 1L;
@Reference
transient private FlagtickIntegrationService flagtickIntegrationService;
@Override
protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response) throws IOException {
if (flagtickIntegrationService == null) {
response.setStatus(HTTP_UNAVAILABLE);
response.getWriter().write("Service unavailable");
return;
}
String userID = CookieUtils.getCookieValue(request, Constants.USER_ID_PARAMETER);
if (StringUtils.isBlank(userID)) {
String errorMsg = "No user ID found for the request";
response.getWriter().append(errorMsg);
response.setStatus(HttpsURLConnection.HTTP_BAD_REQUEST);
return;
}
String token = CookieUtils.getCookieValue(request, Constants.ACCESS_TOKEN);
if (StringUtils.isBlank(token)) {
String errorMsg = "No JWT Token found for the request";
response.getWriter().append(errorMsg);
response.setStatus(HttpsURLConnection.HTTP_BAD_REQUEST);
return;
}
response.setContentType(Constants.CONTENT_TYPE_JSON);
response.setCharacterEncoding(Constants.ENCODING_UTF8);
response.setStatus(HTTP_OK);
Map<String, String> params = new HashMap<>();
params.put(Constants.USER_ID, userID);
UserInformationModel userInformationModel = flagtickIntegrationService.retrieveAccountInformation(params, token);
if (userInformationModel == null) {
CookieUtils.eraseCookie(request, response);
response.setStatus(HTTP_BAD_REQUEST);
response.getWriter().write("Invalid request parameters");
return;
}
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.writeValue(response.getWriter(), userInformationModel);
}
}
First and foremost, let us set up testing environment for LoginServlet in unit test using the JUnit and Mockito frameworks.
@ExtendWith({AemContextExtension.class, MockitoExtension.class})
class LoginServletTest {
private static MockedStatic<CookieUtils> cookieUtilsMockedStatic;
private final AemContext ctx = new AemContext();
@Mock
FlagtickIntegrationService flagtickIntegrationService;
@Mock
MockSlingHttpServletRequest req;
@Mock
MockSlingHttpServletResponse resp;
@InjectMocks
private LoginServlet loginServlet;
..
}
Note: Declaring req and resp is not necessary because we can obtain them from the AemContext.
@BeforeAll
public static void init() {
cookieUtilsMockedStatic = Mockito.mockStatic(CookieUtils.class);
}
@AfterAll
public static void close() {
cookieUtilsMockedStatic.close();
}
@Test
public void doGet() throws IOException {
MockSlingHttpServletRequest request = ctx.request();
MockSlingHttpServletResponse response = ctx.response();
// TODO
}
If we want to conduct unit test for access token, here is an example code in the // TODO section.
cookieUtilsMockedStatic.when(() -> CookieUtils.getCookieValue(request, Constants.USER_ID_PARAMETER)).thenReturn("134");
cookieUtilsMockedStatic.when(() -> CookieUtils.getCookieValue(request, Constants.ACCESS_TOKEN)).thenReturn("");
underTestServlet.doGet(request, response);
String responseContent = response.getOutputAsString();
assertEquals(HttpsURLConnection.HTTP_BAD_REQUEST, response.getStatus());
assertEquals(responseContent, "No JWT Token found for the request");
How can we test the response returned from doGet() in LoginServlet? Here is an example diagram outlining what we will do.
» LoginServletTest.java
String accessToken = "ej...";
String userID = "134";
Map<String, String> promisParams = new HashMap<>();
promisParams.put(Constants.USER_ID_PARAMETER, userID);
UserInformationModel userInformationModel = new UserInformationModel("Flagtick Inc", "Website");
cookieUtilsMockedStatic.when(() -> CookieUtils.getCookieValue(request, Constants.PERSON_ID_PARAMETER)).thenReturn(userID);
cookieUtilsMockedStatic.when(() -> CookieUtils.getCookieValue(request, Constants.TOKEN_COOKIE_NAME)).thenReturn(accessToken);
when(flagtickIntegrationService.retrieveAccountInformation(promisParams, accessToken)).thenReturn(userInformationModel);
loginServlet.doGet(request, response);
String responseContent = response.getOutputAsString();
ObjectMapper mapper = new ObjectMapper();
UserInformationModel result = mapper.readValue(responseContent, UserInformationModel.class);
assertEquals(HttpServletResponse.SC_OK, response.getStatus());
assertEquals(userInformationModel.getFullName(), result.getFullName());
Increasing unit tests for other Java classes can improve code quality and enhance test coverage in an AEM project. For example, consider rewriting unit tests for the CookieUtil class, which includes methods like getCookieValue, setCookieValue, and eraseCookie.
» CookieUtils.java
public class CookieUtils {
public static String getCookieValue(SlingHttpServletRequest request, String cookieName) {
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals(cookieName)) {
return cookie.getValue();
}
}
}
return StringUtils.EMPTY;
}
// TODO
}
» CookieUtilsTest.java
@ExtendWith({
AemContextExtension.class,
MockitoExtension.class})
public class CookieUtilsTest {
private static SlingHttpServletRequest request;
private static SlingHttpServletResponse response;
private static Cookie userIDCookie;
private static Cookie accessTokenCookie;
private static Cookie[] cookies;
CookieUtils cookieUtils;
@BeforeEach
public void setUp() {
request = mock(SlingHttpServletRequest.class);
response = mock(SlingHttpServletResponse.class);
cookieUtils = new CookieUtils();
userIDCookie = new Cookie("UserID", "134");
accessTokenCookie = new Cookie("AccessToken", "ejkdjfds...");
cookies = new Cookie[]{accessTokenCookie, userIDCookie};
when(request.getCookies()).thenReturn(cookies);
}
@Test
public void testGetCookie() {
String cookieName = "UserID";
String expResult = "134";
String result = CookieUtils.getCookieValue(request, cookieName);
assertEquals(expResult, result);
}
// TODO
}
To sum it up, we've shown you how to get the JSON structure of pages or resources like AEM components and experience fragments. Now, you can start writing basic tests for Sling Models and Sling Servlets. But hold on – there's more to explore! In our next article, we'll tackle Unit Testing in depth. We'll focus on creating tests for OSGi services and dig into related files like DTOs and deserializer files.