December 31, 2020
Estimated Post Reading Time ~

Unit testing in AEM - Debugging issues in Test class

This post is for illustrating the possible errors/exception we get while writing Test class. Also highlights the possible ways of creating Test content and the ResourceResolverType available for creating AemContext (from io.wcm framework)

Test content:
  • The most important aspect of Unit Testing is creating Test content which in the AEM sense is to provide a means of the repository at the time of code build phase to test our Java code (written interacting with a repo)
  • For creating the same, we already saw that there are 3 possible APIs - Content Loader, Content Builder, and Resource Builder available.
  • Most often we use Content Loader making use of JSON file at classpath and loading it to dummy content path. Below are the sample snippet making use of the other two APIs.
Content Builder :
private final AemContext aemContext = new AemContext(ResourceResolverType.JCR_MOCK);
ContentBuilder contentBuilder = aemContext.create();
With this contentBuilder object, we can create page, asset, tag etc - Full list of methods available with ContentBuilder API is in doc.

Resource Builder:
private final AemContext aemContext = new AemContext(ResourceResolverType.JCR_MOCK);
ResourceBuilder rescBuilder= aemContext.build();
With this rescBuilder object, we can create resource with properties - Full list of methods available with ResourceBuilder API is in doc.

Resource Resolver Types (as available from io.wcm)
RESOURCERESOLVER_MOCK - new AemContext(ResourceResolverType.RESOURCERESOLVER_MOCK);
  • Mock for Resource Resolver / Factory
  • Access to all Sling API
  • No underlying JCR repository and hence the usage of JCR API is not possible.
    • (Example: Adapting to JCR objects will return null)
  • This type is very fast
  • Usage: If no JCR APIs are involved, we can make use of RESOURCERESOLVER_MOCK while creating AemContext.
JCR_MOCK - new AemContext(ResourceResolverType.JCR_MOCK);
  • JCR In-memory mock as an underlying repository.
  • Support most of the JCR features with an exception of extended features like Eventing, Search, Versioning, etc.
  • This is slow compared to RESOURCERESOLVER_MOCK
  • Usage: If both Sling API and JCR APIs are involved, make use of JCR_MOCK.
JCR_OAK - new AemContext(ResourceResolverType.JCR_OAK);
  • Uses a real JCR Jackrabbit Oak repository.
  • This is slow compared to JCR_MOCK
  • Usage: Common use case would be for logic involving JCR query, JCR Observation
Note: We can create AemContext without specifying ResourceResolverType as well - new AemContext()

Debugging/Troubleshooting:
Null pointer Exception:
  • Let's consider we are writing Test class for Sling Model and in Test class, we have created AemContext, mock content and instantiated the Sling model (via request or resource of aemContext)
  • In case if any of the APIs/objects used in the sling model in -> init method / in constructor injected scenario is not available to Test class, it will result in the Sling Model object being null.
  • If a null check is handled, it will result in the next possible scenario.
Example :
  • In the example below, the "currentPageObj" in the "init" method is an instance variable and is initialized in Injected Constructor.
  • If currentPage object in the constructor (to initialize the "currentPageObj") is not set to AemContext, then currentPage in the constructor is null while the Sling model is instantiated in Test class and hence null for currentPageObj in init method -> Ultimately Sling Model object will be null in Test class.
@Model(adaptables = { Resource.class,
SlingHttpServletRequest.class }, defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
public class SlingModelConstructor {

private Page currentPageObj;

public String getTitle() {
return title;
}

private String title;

/* Constructor Injection */
@Inject
public SlingModelConstructor(@ScriptVariable @Named("currentPage") Page currentPage) {
currentPageObj = currentPage;
}

@PostConstruct
protected void init() {
title = currentPageObj.getTitle();
}

}


In the Test class snippet below, we have used ContentBuilder to create a Page object and is set to AemContext and then Sling Model is instantiated via request from AemContext. In this way, the valid object is created and will be available to access its methods.

@ExtendWith({ AemContextExtension.class, MockitoExtension.class })
class SlingModelConstructorTest {

private SlingModelConstructor modelObj;

private Page page;

private final AemContext aemContext = new AemContext(ResourceResolverType.JCR_MOCK);

@BeforeEach
void setUp() throws Exception {
aemContext.addModelsForClasses(SlingModelConstructor.class);
ContentBuilder cntBuilder = aemContext.create();
page = cntBuilder.page("/content/learnings", "/conf/learnings/templates/content-temp", "Sample Page - Aug 2020");
/* Page is set to AemContext object */
aemContext.currentPage(page);
/* Sling Model Instantiation */
modelObj = aemContext.request().adaptTo(SlingModelConstructor.class);
}

@Test
void testGetTitle() {
String expectedTitle = "Sample Page - Aug 2020";
assertNotNull(modelObj);
String actualTitle = modelObj.getTitle();
assertEquals(expectedTitle, actualTitle);
}

}


Assertion Failed :
  • If the assertion conditions are not satisfied, then it will result in AssertionFailedError.
  • From the literal sense, we know if expected and actual are not the same, then it results in this sort of error.
  • Most often we get this in the below situation.
  • Example:
    • In the same example as above in a Test class -> assume Page object is not set to AemContext -> Sling Model object retrieved -> null check is handled while invoking its method - In this case, it is getTitle() method, it would result in AssertionFailed error. (Expected would be some String while the actual title we get is null)
@ExtendWith({ AemContextExtension.class, MockitoExtension.class })
class SlingModelConstructorTest {

private SlingModelConstructor modelObj;

private Page page;

private final AemContext aemContext = new AemContext(ResourceResolverType.JCR_MOCK);

@BeforeEach
void setUp() throws Exception {
aemContext.addModelsForClasses(SlingModelConstructor.class);
modelObj = aemContext.request().adaptTo(SlingModelConstructor.class); // No Page object set and model is directly instantiated
}

@Test
void testGetTitle() {
String expectedTitle = "Sample Page - Aug 2020";
String actualTitle = null ;
if(null != modelObj){ // modelObj will be null and this block won't execute
actualTitle = modelObj.getTitle();
}
assertEquals(expectedTitle, actualTitle); // actualTitle will be null and hence AssertionFailedError

}

}


Mocking the right resource:
  • If we are to create a mock resource JSON for performing tests, we need to understand which line of code to be tested and the respective resource can be retrieved from the actual content (from repo/CRXDE) using selector - "tidy.-1.json" on the respective resource.
  • By the term "respective resource",
    • It means node/resource of type cq:Page if we are to test page related functionality.
    • It means node/resource of type nt:unstructured/component node under the content path if we are to test component related logic via ValueMap.
Others:
  • Apart from the above, if any of the code statements making use of a specific API is not available within the method under test, then it will throw its respective exception according to the API used.
  • There are cases where certain methods or properties are not available with Testing APIs themselves. One such case which was raised in the Community forum recently, jcr:created property is not accessible
Conclusion:
  • The first immediate reach is AemContext from io.wcm framework, if anything is not accessible within that, explore Mocking related frameworks and its methods -> provide dummy implementation per the code statements.
  • With the understanding of the flow of how to test class works + making use of available Testing/Mocking APIs + method under test + functionality it executes + control flow when a specific line is executed, we will be able to write test class with ease and debug in case of any issues in test class execution.


By aem4beginner

No comments:

Post a Comment

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