I have seen two approaches to writing the JUnit test class for a 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 the 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.
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 the 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>
<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 a test resource structure, so if you want to use test resources for your test classes, you will need to set up the structure in your project.
<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 a test resource 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() the 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.
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() the 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.
No comments:
Post a Comment
If you have any doubts or questions, please let us know.