
When we develop AEM components, we add some logic into them. This logic can include a variety of source code: helpers, utils, or code designed specially for a component. After developing a number of components, we need to verify the logic. Any refactoring, any changes in helper and util classes, requires us to validate existing source code, thus verifying that the code is in the expected state. JUnit tests help us do this.
However, in the process of developing the JUnit tests, a few issues arose. For instance: WCMUsePojo (a base class for components) has many static methods which have to be mocked to perform testing. We have to load content and test our component against this content to validate its logic.
For our unit tests, we use an open source project called wcm.io as well as PowerMock. Wcm.io provides various useful AEM mock objects. PowerMock helps to override static and final methods in classes for mocking. PowerMock takes precedence over the Mockito framework in this process.
Let’s imagine that this is our component class:
1 2 3 |
public class OurSuperComponent extends WCMUsePojo { } |
To write a unit test for a class, the first thing we will do is annotate a test class with the following annotations:
1 2 3 4 |
@RunWith(PowerMockRunner.class) @PrepareForTest({OurSuperComponent.class, RequestHelper.class}) public class OurSuperComponentTest extends ComponentBaseTest {} |
@RunWith specifies a runner for JUnit; @PrepareForTest lists a number of classes which have final or static methods to mock.
Here is the component base class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public abstract class ComponentBaseTest { protected final Class componentClass = (Class) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0]; protected final AemContext context = new AemContext(ResourceResolverType.RESOURCERESOLVER_MOCK); protected T component; protected Resource resource; @Before public void setUp() throws Exception { // all component’s methods are mock by default component = PowerMockito.mock(componentClass); resource = PowerMockito.mock(Resource.class); } } |
AemContext is a useful mock object. It could contain test content. Here is a document describing how to load it.
Next we can get ResourceResolver.
1 |
ResourceResolver resourceResolver = context.resourceResolver(); |
And mock it for further use while testing in a component:
1 2 3 |
when(resource.getResourceResolver()).thenReturn(resourceResolver); when(component.getResourceResolver()).thenReturn(resourceResolver); when(slingHttpServletRequest.getResourceResolver()).thenReturn(resourceResolver); |
In the base class we mock our test component:
1 |
component = PowerMockito.mock(componentClass); |
Now all its methods are mocked. To call a real method to test it, we would write:
1 |
PowerMockito.doCallRealMethod().when(component).activate(); |
If a test method is private and we have to test it or call it, we can write:
1 |
PowerMockito.when(component,"getItem",anyObject()).thenCallRealMethod(); |
How to mock a static method?
For instance, let’s say we have a util class with a static method which is used in our component:
1 2 3 4 5 |
public final class LinkUtil { public static String localizeUrl(final ResourceResolver resourceResolver, final String currentPagePath, final String link) { // method body } } |
In a unit test we would write:
1 2 |
PowerMockito.mockStatic(LinkUtil.class); when(LinkUtil.localizeUrl(anyObject(), anyString(), anyString())).thenReturn("/content/page.html"); |
PowerMockito.mockStatic mocks all the methods in a specified class. If you want to mock one method, use PowerMockito.spy.
Source Code
1. Component Class
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
public class MySuperComponent extends WCMUsePojo { private String title; private String upperTitle; private String resourcePath; @Override public void activate() throws Exception { title = getProperties().get("./title", StringUtils.EMPTY); resourcePath = getProperties().get("./resourcePath", StringUtils.EMPTY); upperTitle = getUpperString(title); } private String getUpperString(String title) { if (StringUtils.isNotBlank(title)) { return StringUtils.upperCase(title); } return title; } public String getTitle() { return title; } public String getUpperTitle() { return upperTitle; } public String getResourcePath() { return resourcePath; } public String getCurrentPagePath() { return getCurrentPage().getPath(); } public String getResourceTitle() { Resource resource = getResourceResolver().getResource(resourcePath + "/jcr:content"); return resource.getValueMap().get("title").toString(); } } |
2. Base Test Class
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
public abstract class ComponentBaseTest { protected T component; protected ValueMap valueMap; protected SlingScriptHelper slingScriptHelper; protected SlingHttpServletRequest slingHttpServletRequest; protected ResourceResolver resourceResolver; protected SightlyWCMMode sightlyWCMMode; protected Resource resource; protected MockRequestPathInfo requestPathInfo; private boolean firstRun = true; protected final Class componentClass = (Class) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0]; protected final AemContext context = new AemContext(ResourceResolverType.RESOURCERESOLVER_MOCK); protected void runOnceBeforeTests() {} @Before public void setUp() throws Exception { runOnce(); component = PowerMockito.mock(componentClass); resource = PowerMockito.mock(Resource.class); resourceResolver = context.resourceResolver(); valueMap = new ValueMapDecorator(new HashMap<>()); slingScriptHelper = PowerMockito.mock(SlingScriptHelper.class); slingHttpServletRequest = PowerMockito.mock(SlingHttpServletRequest.class); sightlyWCMMode = PowerMockito.mock(SightlyWCMMode.class); when(component.getResource()).thenReturn(resource); when(resource.getResourceResolver()).thenReturn(resourceResolver); when(component.getResourceResolver()).thenReturn(resourceResolver); when(component.getProperties()).thenReturn(valueMap); when(component.getSlingScriptHelper()).thenReturn(slingScriptHelper); when(component.getRequest()).thenReturn(slingHttpServletRequest); when(component.getWcmMode()).thenReturn(sightlyWCMMode); requestPathInfo = new MockRequestPathInfo(); when(slingHttpServletRequest.getRequestPathInfo()).thenReturn(requestPathInfo); when(slingHttpServletRequest.getResourceResolver()).thenReturn(resourceResolver); } private void runOnce() { if (firstRun) { firstRun = false; runOnceBeforeTests(); } } } |
3. JUnit Test Class
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
@RunWith(PowerMockRunner.class) @PrepareForTest({ MySuperComponent.class, StringUtils.class}) public class MySuperComponentTest extends ComponentBaseTest { @Override protected void runOnceBeforeTests() { initContent(); } public void initContent() { context.load().json("/test/TestNode.json", "/content/us/en/testPage/jcr:content"); } @Test public void testActivate() throws Exception { PowerMockito.doCallRealMethod().when(component).activate(); PowerMockito.when(component, "getUpperString", anyString()).thenCallRealMethod(); PowerMockito.doCallRealMethod().when(component).getTitle(); PowerMockito.doCallRealMethod().when(component).getResourcePath(); final String resourcePathTest = "/content/us/en/testPage"; final String titleTest = "SimpleTitle"; valueMap.put("./resourcePath", resourcePathTest); valueMap.put("./title", titleTest); component.activate(); assertEquals(resourcePathTest, component.getResourcePath()); assertEquals(titleTest, component.getTitle()); } @Test public void testPrivateMethodGetUpperString() throws Exception { PowerMockito.doCallRealMethod().when(component).activate(); PowerMockito.doCallRealMethod().when(component).getUpperTitle(); PowerMockito.when(component, "getUpperString", anyString()).thenCallRealMethod(); final String titleTest = "SimpleTitle"; valueMap.put("./title", titleTest); component.activate(); //returns in upper case assertEquals(titleTest.toUpperCase(), component.getUpperTitle()); // Static mock StringUtils PowerMockito.mockStatic(StringUtils.class); // mock all methods in StringUtils when(StringUtils.isNotBlank(anyString())).thenReturn(Boolean.FALSE); component.activate(); // returns the result string in lower case assertEquals(titleTest, component.getUpperTitle()); // Partial static mock StringUtils PowerMockito.spy(StringUtils.class); // mock only the following methods in StringUtils when(StringUtils.upperCase(anyString())).thenReturn("HackedString"); component.activate(); // returns the mocked string assertEquals("HackedString", component.getUpperTitle()); } @Test public void testGetResourceTitle() throws Exception { PowerMockito.doCallRealMethod().when(component).activate(); PowerMockito.doCallRealMethod().when(component).getResourceTitle(); // Necessary test page content has been loaded in initContent method final String resourcePathTest = "/content/us/en/testPage"; valueMap.put("./resourcePath", resourcePathTest); component.activate(); String result = component.getResourceTitle(); assertEquals("Resource test title", result); } } |
4. Test Page Content [TestNode.json]
1 2 3 4 5 |
{ "jcr:primaryType": "cq:PageContent", "title": "Resource test title", "jcr:title": "Page title" } |
Author: Alexander Bestsenny
Are you a UI developer?
We have open positions!