As you may know, unit testing and test-driven development (TDD) are important for making sure your code complies with the design, is scalable among your team, and provides automated regression. Often times, the JUnit test and component back-end Java code come hand in hand. An AEM developer who writes the component logic is also responsible to write the JUnit test code for the class. In my previous blog post, I talked about how you can switch from WCMUsePojo API to Sling Models for Adobe Experience Manager (AEM) component. Here in part two, I am going to discuss how you can make the switch in terms of the JUnit test.
I have seen two approaches to writing the JUnit test class for component class that extends the WCMUsePojo class. One is using Mockito and mocking each AEM/Sling object (i.e. bindings, resource, page, properties, SlingHttpServletRequest, SlingHttpServletResponse…) you will need for your test class, wiring those mocks with each other using Mockito’s when().thenReturn()
, or PowerMockito.doReturn().when()
, and activating the ComponentUse class with the properties and bindings passed in from each test case. The second approach is using AemContext class from wcm.io and setting it as your JUnit test rule, and then using a test content json file in your Test Resources Root folder to provide test page/resource content for your test cases.
If you haven’t heard of wcm.io, it’s an open-source project that is hosted on GitHub and provides handy libraries and extensions for AEM developers. We will be focusing on the AEM Mocks feature in wcm.io specifically, as it can be used and helpful for both WCMUsePojo and Sling Models test classes.
One of the reasons I like using AEM Mocks here is that it’s very robust and provides access to all mocked environments in Sling project (Sling Mocks, OSGI Mocks and JCR Mocks) and also all the context objects (i.e. SlingBindings, resource, page, properties, SlingHttpServletRequest…), so you don’t need to create and wire mocked objects individually and you can then write cleaner test codes. Secondly, it fully supports Sling Models.
In here, I am taking the title component I developed from my previous blog as an example. I have written two sample JUnit test classes, one is for TitleUse.java, which extends WCMUsePojo, the other is for TitleModel.java, which is a Sling Models class. You can find all source code in my GitHub project.
Note: this was tested on AEM 6.2, 6.3
When you use the AemContext object in your test class, and your project skeleton was generated by Adobe Maven Archetype 10, like mine, you may find several issues when you run the test code. Those can be fixed by modifying the Maven dependencies in your POM file. Issues:
1. java.lang.NoClassDefFoundError: org/junit/rules/TestRule
java.lang.ClassNotFoundException: org.junit.rules.TestRule
Resolved by: validating the maven dependencies of test scope, here’s a working copy in my parent POM:
<dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-simple</artifactId> <version>1.7.6</version> <scope>test</scope> </dependency> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>1.10.19</version> <scope>test</scope> </dependency> <dependency> <groupId>org.powermock</groupId> <artifactId>powermock-api-mockito</artifactId> <version>1.6.4</version> <scope>test</scope> </dependency> <dependency> <groupId>org.powermock</groupId> <artifactId>powermock-module-junit4</artifactId> <version>1.6.4</version> <scope>test</scope> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-imaging</artifactId> <version>1.0-R1534292</version> <scope>test</scope> </dependency> <dependency> <groupId>io.wcm</groupId> <artifactId>io.wcm.testing.aem-mock</artifactId> <version>2.1.0</version> <scope>test</scope> <exclusions> <exclusion> <groupId>org.apache.commons</groupId> <artifactId>commons-imaging</artifactId> </exclusion> </exclusions> </dependency> <!-- for testing we need the new ResourceTypeBasedResourcePicker --> <dependency> <groupId>org.apache.sling</groupId> <artifactId>org.apache.sling.models.impl</artifactId> <version>1.3.0</version> <scope>test</scope> </dependency>
- lang.NoSuchMethodError:
org.osgi.framework.BundleContext.getServiceReference(Ljava/lang/Class;)Lorg/osgi/framework/ServiceReference;
Resolved by: validating the version of the osgi-core library, here’s a working copy in my parent POM:
<dependency> <groupId>org.osgi</groupId> <artifactId>osgi.core</artifactId> <version>6.0.0</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.osgi</groupId> <artifactId>osgi.cmpn</artifactId> <version>6.0.0</version> <scope>provided</scope> </dependency>
- lang.NoSuchMethodError: org.slf4j.helpers.MessageFormatter.arrayFormat(Ljava/lang/String;[Ljava/lang/Object;]Lorg/slf4j/helpers/FormattingTuple;
Resolved by: validating the version of the slf4j library, here’s a working copy in my parent POM:
<dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.6</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-simple</artifactId> <version>1.7.6</version> <scope>test</scope> </dependency>
The package version numbers above are based on AEM 6.3, since I am using Sling Models API 1.3 features like associating a model class with a resource type and exporter framework. If you are on AEM 6.2 or lower, you may find some imported packages cannot be resolved in your bundle, you can either manually install the Sling Models 1.3 bundle, or adjust your package version number. Simply check the unresolved bundle in Package Dependencies (http://localhost:4502/system/console/depfinder) and locate the maven dependency in your POM file. Also, be aware of the version number of uber-jar or other bundles to provide AEM APIs and match those with your AEM version.
The Adobe Maven Archetype (10 or 11) didn’t generate test resources structure, so if you want to use test resources for your test classes, you will need to set up the structure in your project.
Basically you will create a folder under /core/src/test/resources, and put your component test resources in there. In IntelliJ, after you create the directory, you can mark it as Test Resource Root. The resource files you put in the resources folder can be loaded from your test class.
Now that you have everything set up for you to write JUnit test cases for your component class, here’re the steps:
1. Create the test class in the same package path under /core/src/test/java
.
2. Know the JUnit annotations that you are going to use.
a. If you are using wcm.io’s AEM mock context object, you will need the @Rule annotation. The rule will run any Before methods, then the Test method, and finally any After methods, throwing an exception if any of these fail, so you don’t need to define the object repeatedly in those scenarios.
b. @Before
annotation is used for set up methods (like assigning common mocked values, loading test content and binding it to Sling request variables) to be called before the actual test cases run.
c. @Test
annotation holds statements for each test case to be run for the test class.
3. For Sling Models specifically:
a. If you are using wcm.io’s AEM mock context object, you will need to register models from package by context.addModelsForPackage("org.myorg.blog.core.models");
b. If you are using resourceType feature in Sling Models API 1.3, you may register ResourceTypeBasedResourcePicker service in mocked OSGI environment, by context.registerService(ImplementationPicker.class, new ResourceTypeBasedResourcePicker());
c. If you are using @ScriptVariable
in your Sling Models class to provide script objects (i.e. currentPage, properties…), you may use SlingBindings class in your test class to add those objects by
slingBindings = (SlingBindings) context.request().getAttribute(SlingBindings.class.getName());
slingBindings.put(WCMBindings.
CURRENT_PAGE
, page);
d. Call the Sling Models class by underTest = context.request().adaptTo(TitleModel.class);
4. Write different test cases based on your code design and logic
5. Run your unit test class
Differences between writing test class for WCMUsePojo (ComponentUseTest.java) and for Sling Models (ComponentModelTest.java):
1. In ComponentUseTest you mock/spy an instance of your use class, whereas in ComponentModelTest you call the Sling Models class directly;
2. In ComponentUseTest you heavily rely on Mockito/PowerMockito to mock the objects returned from WCMUsePojo APIs, whereas in ComponentModelTest you can just set up the context objects and Sling Models will be able to inject the properties/script variables from those context objects;
3. In ComponentUseTest you initialize the use class by calling activate()
method, whereas in ComponentModelTest you initialize the Sling Models class by calling adaptTo()
method.
I hope after this article, you get more knowledge about writing JUnit test class for your component Java code and know the difference between writing test class for WCMUsePojo and for Sling Models. If you want to know more about unit testing and AEM mocks, I found these two decks online that are helpful, one is an AEM GEMS resource, the other is an adaptTo() presentation. And if you missed my first post on switching from WCMUsePojo API to Sling Models in Adobe Experience Manager, you can read it here.
Let me know what you are interested to know more about around WCMUsePojo and Sling Models. Happy coding.
Hi,
Thanks for the article.
I have a situation where I have to adapt the request more than once in order to cover multiple scenarios.
For eg. I have set a currentpage for context i.e. context.currentPage(“a/b/c.html) and then I adapt the request -> context.request().adaptTo(TitleModel.class);
Now I am trying to cover another scenario where currentPage is having different properties so I change it in context i.e context.currentPage(“d/e/f.html”) but since the request has not yet changed, when I again try to adapt using context.request().adaptTo(TitleModel.class); I get the same object. 🙁 and my second scenario does not get covered. What would be the best way to reset this request in order to get different model object adjusted to this new page that I have set in context ?