December 31, 2020
Estimated Post Reading Time ~

Unit Testing in AEM - Hands on

This post is about hands-on on Unit Testing Java class, part of an AEM application.
We will be using AEM Mocks from io.wcm.testing.mock.aem.junit5.* and Mockito framework - org.mockito.*

JUnit Version: JUnit 5
AEM Maven archetype: 22
IDE: Eclipse
Testing related maven dependency: (available by default in AEM Maven archetype)
  • JUnit5 (org.junit)
  • AEM mocks (io.wcm.testing.aem-mock.junit5)
  • Mockito framework (org.mockito)
We mostly follow the "Implementation first development" approach - Desired functionality is first coded and then a test case is written for the same.

For creating Test Java class,
  • Create Test Java Class
  • Create AemContext
  • Implement a setUp method (creating a test fixture, annotated with @BeforeEach)
  • Implement methods to be tested. (annotated with @Test)
Create Test Java Class:
In Eclipse IDE, right-click on the Java class to be tested ->
New -> Other -> JUnit -> JUnit Test case

We will have options to choose
  • JUnit version (as we have JUnit5 defined in project pom, JUnit Jupiter should be selected)
  • Displays the location of the Test file (packages/filename)
  • Methods to be part of Test class file - setUp method is selected by default where we define test fixture
  • Method to be considered for the test - List of methods in our Java class to be tested will be available for selection.
  • After this initial selection, we have our test class file generated. (JUnit level annotations like @Test, @BeforeEach will be automatically added based on our selection in the process of creating Test Java class)
Create AemContext:
In order to initialize and make use of AEM context throughout all the test methods/set up method in a Test class file,
  • We annotate the class using
    • @ExtendWith(AemContextExtension.class)
  • Create instance variable for AemContext object as follows
    • private final AemContext aemContext = new AemContext();
Implement setUp()/@BeforeEach method:
As with the name of annotation(@BeforeEach for setUp method), this is called before executing each test method.
Note: For illustration, we will consider Sling Models - commonly used as part of AEM component development
In our case,
  • Class under test(Sling Model) to be registered/added to AemContext
    • The below line will register the class with @Model annotation. We have another method from AemContext to add models at the package level - addModelsforPackage. 
        aemContext.addModelsForClasses(CompUnderTestModel.class);
  • Mock resource definitions set up are to be done here.
  • For models adapted as a resource (where we act upon the resource in the repo), we are creating a mock resource structure in the form of JSON to test the actions on the resource.
  • The below line will add the created mock resource structure to AemContext to act upon.
private final String CONTENT_PATH = "/content/learnings/compundertestmodel";
private final String MOCK_RESC_JSON = "/learnings/core/models/CompUnderTestModel.json";
aemContext.load().json(MOCK_RESC_JSON, CONTENT_PATH);
MOCK_RESC_JSON is the json file created under class path - /src/test/resources (/src/test/resources/learnings/core/models/CompUnderTestModel.json)
CONTENT_PATH - Any meaningful dummy content path.

  • Alternatively, we have few other methods from AemContext for creating test content - create/build for creating Content Builder and Resource Builder objects respectively
Implement/Understanding @Test methods (behind the scene):
  • As part of the mock resource definition in the previous step, we loaded the JSON to the dummy content path. Now the logic in the test method would be to access that dummy content path and its loaded resource props to test against the expected.
  • Example:
    • The sling model is written to retrieve a title.
    • Author the component in a page, fulfill the dialog values.
    • Get the JSON structure of authored resource (path of the component authored under specific page followed by .tidy.json)
    • Use that for creating a mock resource JSON file.
    • Let's say, the authored Title in the dialog to be "Sample Title".
    • Then in the test method to test assertEquals(expected, actual), set the
      • expected String -> Hardcode the value, the same as the one authored in the component- "Sample Title"
        • String expectedString = "Sample Title";
      • actual String -> Retrieve the title from aemContext(which is loaded with mock resource structure at dummy content path)
        • Resource currentResc = aemContext.currentResource("/content/learnings/compundertestmodel");
        • ValueMap currentRescVal = currentResc.getValueMap();
        • String actualString = currentRescVal.get("title", String.class);
        • assertEquals(expectedString, actualString);
  • This distinction of actual and expected + each and every line of our actual code under test will help arrive at the desired test fixture and to perform other related tests like assertNotEquals/ assertTrue/ assertFalse and so on. In this case, setting up the aemContext correctly and hence implement test methods accordingly.
Few sample snippets/test statements for reference:
Create Page :

pageManager() is available in the aemContext and hence we are using it to test the "create" method of the same.

CreatePage.java
/* Create Page */
PageManager pageMgr = rescResolver.adaptTo(PageManager.class);
try {
if(null != pageMgr) {
String pageName = pageTitle.toLowerCase().replace(" ", "_");
Page newPage = pageMgr.create(pagePath, pageName, templatePath, pageTitle, true);
if(null != newPage) {
createdPagePath = newPage.getPath();
}
}

} catch (WCMException e) {
LOG.error("WCM Exception={}", e.getMessage());
}

CreatePageTest.java
private final String PAGE_TITLE = "Page Creation For Test";
private final String TEMPLATE_PATH = "/conf/learnings/settings/wcm/templates/content-page-template";
private final String CONTENT_PATH = "/content/learnings/compundertestmodel"; //any dummy content path
private final AemContext aemContext = new AemContext();
@Test
void testGetCreatedPagePath() {
String pageName = getPageName();
String expectedPath = CONTENT_PATH + "/page_creation_for_test";
String actualPath = "";
try {
// pageManager() is available in aemContext. Hence used directly to test create page functionality
Page page = aemContext.pageManager().create(CONTENT_PATH, pageName, TEMPLATE_PATH, PAGE_TITLE, true);
if (null != page) {
actualPath = page.getPath();
}
assertEquals(expectedPath, actualPath);
} catch (WCMException e) {
System.out.println("WCMException=" + e.getMessage());
}

}
private String getPageName() {
String pageName = PAGE_TITLE.toLowerCase().replace(" ", "_");
return pageName;
}


Create Tag by Title:
Tag Manager is not available in aemContext and hence
  • we annotate the class with MockitoExtension along with AemContextExtension which will be @ExtendWith({ AemContextExtension.class, MockitoExtension.class })
  • we mock the API using @Mock from org.mockito
  • provide dummy implementation via its "when" and "thenReturn" methods.
Mockito throws an UnsupportedStubbingException, when an initialized mock is not used in test methods. To skip this exception/validation, we are using "lenient".

CreateTagByTitle.java
TagManager tagMgr = rescResolver.adaptTo(TagManager.class);
try {
Tag tag = tagMgr.createTagByTitle(pageTitle);
if(null != tag) {
createdTagPath = tag.getPath();
}

} catch (AccessControlException e) {
LOG.error("AccessControlException={}", e.getMessage());
} catch (InvalidTagFormatException e) {
LOG.error("InvalidTagFormatException={}", e.getMessage());
}


CreateTagByTitleTest.java
private final String PAGE_TITLE = "Page Creation For Test";
private final String TAGS_DEFAULT = "/content/cq:tags/default";
/* Mock the desired APIs - Starts */
@Mock
private TagManager tagMgr;

@Mock
private Tag tag;
/* Mock the desired APIs - Ends */
@BeforeEach
void setUp() throws Exception {
aemContext.addModelsForClasses(CompUnderTestModel.class);
/* Provide dummy implementation to the mocked APIs - Starts */
lenient().when(tagMgr.createTagByTitle(PAGE_TITLE)).thenReturn(tag);
lenient().when(tag.getPath()).thenReturn(TAGS_DEFAULT + "/" + getPageName());
/* Provide dummy implementation to the mocked APIs - Ends */
}

@Test
void testGetCreatedTagPath() {
String actualTagPath = "";
String expectedTagPath = TAGS_DEFAULT + "/" + getPageName();
try {
Tag tag = tagMgr.createTagByTitle(PAGE_TITLE);
if (null != tag) {
actualTagPath = tag.getPath();
}
assertEquals(expectedTagPath, actualTagPath);
} catch (AccessControlException e) {
System.out.println("AccessControlException=" + e.getMessage());
} catch (InvalidTagFormatException e) {
System.out.println("InvalidTagFormatException=" + e.getMessage());
}
}
private String getPageName() {
String pageName = PAGE_TITLE.toLowerCase().replace(" ", "_");
return pageName;
}

A full list of available methods from AemContext is available in the API doc
Complete Test Java class file for a Sling Model is explained with Video demo in WKND Tutorial

This is more of a starter to write Test Java class and we have many other options as part of Aem Mocks (from io.wcm) and several use cases like registering OSGI service, test class for servlets and so on which will be covered in upcoming posts.


By aem4beginner

No comments:

Post a Comment

If you have any doubts or questions, please let us know.