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 the 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)
A quick recap of the 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
The test fixture is a fixed state of objects in which tests are run. One of the methods it includes which is relevant for us is the setUp method that we write in Test class.(Annotated with @BeforeEach, JUnit5 / @Before, JUnit 4)
Test class has methods to be tested which are annotated with @Test
Test Runner
- Is used to execute the test cases and is defined with help of @RunWith Annotation
- There are several runner implementations available based on the need for custom test execution. One such which is related to our subject is MockitoJunitRunner
- 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.
- The runner has then instantiated which in turn instantiates the Test class and hence executes test methods per the annotations defined.
MockitoJunitRunner:
- Mockito is a framework for mocking - org.mockito.junit.MockitoJUnitRunner
Mocking:
- Mocking provides dummy implementation to an interface.
- Creating mock objects for an interface/proxy for the 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. Let's say,
- sampleService.getAPIUrl()
- With this setup, when we write a Test class for Sling Model, we need to have the 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. A 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)
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 the build phase.
Given this understanding, for writing test class and hence executing the 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
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 a case, we might need to use MockitoJunitRunner along with AemContext.
Need for mocking resource definitions(JSON) in the class path :
Let's say, we have a logic to create a page programmatically in a specific location in the repo.
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 a page in the repository successfully.
For this, we can mock the content hierarchy with respective properties.
Note:
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 the build phase.
Given this understanding, for writing test class and hence executing the 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 the case of JUnit 4.
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 a case, we might need to use MockitoJunitRunner along with AemContext.
- Mock the service for which implementation of a specific method is not available.
- Provide the dummy implementation and then register the respective service to AemContext.
Need for mocking resource definitions(JSON) in the class path :
Let's say, we have a logic to create a page programmatically in a specific location in the repo.
- pageMgr.createPage(...)
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 a 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 the 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");
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 a Test class
- Use that context object to gain access to mocked Sling, OSGI, JCR APIs, and AEM specific APIs
- In case any of the mocked APIs (that wcm.io supports) doesn't have 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 the previous step) to our AemContext.
With this high-level understanding of all the above, we will be able to write a test class for our Java program as part of the AEM application with ease.
Follow up post will be less of theory and more of coding with a sample test case.
Follow up post will be less of theory and more of coding with a sample test case.
No comments:
Post a Comment
If you have any doubts or questions, please let us know.