Unit testing in AEM - Introduction

This post is about an introduction to Unit testing the Java class part of AEM application by starting with quick recap of JUnit framework followed by Mocking and APIs available specific to AEM with respect to testing. 

Unit Testing in AEM

Java class that we write as part of AEM involves Sling API/JCR API/AEM related APIs and it all ultimately targets the content on our repository.  In other words, the logic revolves around the content which in AEM context, is a Resource/Node and its related properties (may it be a Sling model/WCMUsePojo/Sling Servlets/OSGI component/any related for that matter) 

Quick recap of basics:
JUnit is the testing framework for Java and is available under the package - org.junit.*
It provides Test Fixture, Test Runner and Test class
  • Test fixture is fixed state of objects in which tests are run. One of the methods it includes which is relevant for us is setUp method that we write in Test class.(Annotated with @BeforeEach, JUnit5 / @Before, JUnit 4)
  • Test class has methods to be tested which is annotated with @Test
  • Test Runner 
    • Is used to execute the test cases and is defined with help of @RunWith Annotation
    • There are several runner implementation available based on the need for custom test execution. One such which is related to our subject is MockitoJunitRunner 
  • JUnitCore 
    • Is used to start the Test class (As we execute the test in build phase or if we run as JUnit Test in IDE, it is by means of this JUnitCore class - JUnitCore.runClasses(OurTestClass.class) behind the scene)
    • It uses Java Reflection to find the runner of the Test class.
    • Runner is then instantiated which in turn instantiates the Test class and hence execute test methods per the annotations defined.
MockitoJunitRunner:
  • Mockito is a framework for mockingorg.mockito.junit.MockitoJUnitRunner
Mocking:
  • Mocking provides dummy implementation to an interface.
  • Creating mock objects for an interface / proxy for an actual implementation of an interface.
Mocking Example:
  • We have an OSGI service(say, SampleService) that exposes OSGI config values and is referenced in a Sling Model.
  • SampleServiceImpl will have logic to get the OSGI config values from its Activate method.
  • Sling Model will have a piece of code to get OSGI config value from the service it references. Lets say,
    • sampleService.getAPIUrl()
  • With this set up, when we write a Test class for Sling Model, we need to have a means to instruct the action to happen when the above line is executed. (that is the implementation for SampleService to use in the Test class)
  • For this reason we first mock the service referenced using @Mock annotation. 
    • @Mock
    • private SampleService sampleService;
  • With the help of  "when" and  "thenReturn" methods, we provide the implementation to the mocked interface. 
    • when(sampleService.getAPIUrl()).thenReturn("provide a URL that you actually have as part of config");
  • Actual Implementation :
    • Retrieves APIUrl from config available in activate method of an OSGI component/service implementation
  • Mocked/Proxy Implementation:
    • Hardcoding/providing the direct Url/anything as defined in thenReturn method
Mocks available under Apache Sling project
  • Sling Mocks
  • OSGI Mocks 
  • JCR Mocks 
AEM Mocks : (By wcm.io)
  • AemContextExtension in case of JUnit 5, @Rule named AemContext in case of JUnit 4
  • Access to all above mocks (from Apache Sling project)
  • Access to AEM specific APIs - AEM WCM APIs, AEM DAM APIs etc. List of all supported APIs are listed here
Other Mocking related implementation:
  • We have few other Mocking related implementations (To name a few EasyMock, PowerMock, JMock etc) Out of that, would like to mention about PowerMock framework which is used to mock static, final and private methods. (this is not possible directly with Mockito Framework)
How Test class in AEM project works:
  • As the Test class is executed as part of the build phase, there is no means of AEM repo set up available at the time of build phase.
  • Given this understanding, for writing test class and hence executing test, we need to mock/load the resource definitions (in the form of JSON) to the path say "/content".
  • With AEM Mocks available from wcm.io framework (which has mock implementations of Sling API, JCR API, OSGI and AEM related APIs), we can go about using 
    • AemContextExtension via @ExtendWith Annotation in case of JUnit 5.
    • @Rule named AemContext in case of JUnit 4. 
  • We will then use this AemContext object's methods to get resourceResolver / set currentResource to the path we created in class path / can load the JSON to this path and so on. 
  • In short, any operations that we would do against a repo is provided by this AemContext object. 
  • There are some exceptional cases where not all methods of service provided by AemContext is implemented. In such case, we might need to use MockitoJunitRunner along with AemContext
    • Mock the service for which implementation of specific method is not available. 
    • Provide the dummy implementation and then register the respective service to AemContext.
  • Note : This exceptional case is highlighted in WKND tutorial Unit Testing example.

Need for mocking resource definitions(JSON) in class path :
  • Lets say, we have a logic to create page programmatically in a specific location in the repo. 
    • pageMgr.createPage(...)
  • When the code is built and deployed in AEM without any errors/bundle is active, it creates page in desired location as programmed. 
  • Now when it comes to testing, which happens in an IDE as we trigger the build (module test is called), there is no repository set up available to act on. In particular, there is no means to test if the line - pageMgr.createPage(...) will help create page in the repository successfully.
  • For this, we can mock the content hierarchy with respective properties. 
    • Let say, our Sling Model under test is available in com.aem.learnings.models.SampleModel
    • Then under /core/src/main/resources, in the path of Sling Model, we can place the JSON file (That is /core/src/main/resources/com/aem/learnings/models/SampleModel.json)
    • With help of AemContext object, we can load this JSON to the path named - "/content") as follows
      • ctx.load().json("/com/aem/learnings/models/SampleModel.json", "/content");
  • In case of createPage, we can use this as starting point and write logic accordingly. Similarly, delete/ modify/ properties retrieval and all related operations can happen against this.
  • Note :
    • /content that we used can be any meaningful custom path, need not be /content always
Conclusion/Brief flow:
  • AemContext from wcm.io can be used for writing Test class 
  • Use that context object to gain access to mocked Sling, OSGI, JCR APIs and AEM specific APIs
  • In case of any of the mocked APIs (that wcm.io supports) doesn't has any method implementation, we need to mock the respective API explicitly via @Mock (using MockitoJUnitRunner)
  • Based on our code flow of class under test, we need to register the mocked objects (as in previous step) to our AemContext.
With this high level understanding on all the above, we will be able to write Test class for our Java program as part of AEM application with ease. 
Follow up post will be less of theory and more of coding with sample test case.

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