I am having some trouble testing a Sling model: the currentPage is not getting injected for some reason.
My Sling model looks like this:
@Model( adaptables = { SlingHttpServletRequest.class, Resource.class },
resourceType = MyModel.RESOURCE_TYPE)
public class MyModel {
public static final String RESOURCE_TYPE = "myproject/components/renderer";
@Inject
private Page currentPage;
// Model methods, etc.
}
I writing some JUnit tests for it, like so:
@RunWith(MockitoJUnitRunner.class)
public class MyModelTest {
@Rule
public final AemContext context = new AemContext();
@Mock
private SlingHttpServletRequest request;
private static final String RESOURCE_PATH = "/content/myproject/jcr:content/myModel";
private static final String PAGE_PATH = "/content/common/page";
private MyModel myModel;
@Before
public final void setUp() throws Exception {
context.load().json("/models/MyModel.json",RESOURCE_PATH);
context.load().json("/common-page.json", PAGE_PATH);
Resource pageResource = context.resourceResolver().getResource(PAGE_PATH);
Page page = pageResource.adaptTo(Page.class);
context.currentPage(page);
context.addModelsForClasses(MyModel.class);
when(request.getResource()).thenReturn(context.resourceResolver().getResource(RESOURCE_PATH));
myModel = request.getResource().adaptTo(MyModel.class);
}
@Test
public void simpleLoadTest(){
assertNotNull(myModel);
}
}
And this is is the error I am getting:
[main] WARN org.apache.sling.models.impl.ModelAdapterFactory - Could not adapt to model
org.apache.sling.models.factory.MissingElementsException: Could not inject all required fields into class com.myproject.common.core.models.MyModel
at org.apache.sling.models.impl.ModelAdapterFactory.createObject(ModelAdapterFactory.java:558)
at org.apache.sling.models.impl.ModelAdapterFactory.internalCreateModel(ModelAdapterFactory.java:319)
at org.apache.sling.models.impl.ModelAdapterFactory.getAdapter(ModelAdapterFactory.java:195)
at org.apache.sling.testing.mock.sling.MockAdapterManagerImpl.getAdapter(MockAdapterManagerImpl.java:146)
at org.apache.sling.testing.mock.sling.ThreadsafeMockAdapterManagerWrapper.getAdapter(ThreadsafeMockAdapterManagerWrapper.java:46)
at org.apache.sling.api.adapter.SlingAdaptable.adaptTo(SlingAdaptable.java:104)
at org.apache.sling.testing.resourceresolver.MockResource.adaptTo(MockResource.java:110)
at uk.co.restaurants.common.core.models.MyModelTest.setUp(MyModelTest.java:44)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:47)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:44)
at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:24)
at org.junit.rules.ExternalResource$1.evaluate(ExternalResource.java:48)
at org.junit.rules.RunRules.evaluate(RunRules.java:20)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:271)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:70)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:50)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:238)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:63)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:236)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:53)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:229)
at org.junit.runners.ParentRunner.run(ParentRunner.java:309)
at org.mockito.internal.runners.JUnit45AndHigherRunnerImpl.run(JUnit45AndHigherRunnerImpl.java:37)
at org.mockito.runners.MockitoJUnitRunner.run(MockitoJUnitRunner.java:62)
at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:86)
at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:459)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:678)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:382)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:192)
Suppressed: org.apache.sling.models.factory.MissingElementException: Could not inject private com.day.cq.wcm.api.Page com.myproject.common.core.models.MyModel.currentPage
at org.apache.sling.models.impl.ModelAdapterFactory.createObject(ModelAdapterFactory.java:562)
... 34 more
Caused by: org.apache.sling.models.factory.ModelClassException: No injector returned a non-null value!
at org.apache.sling.models.impl.ModelAdapterFactory.injectElement(ModelAdapterFactory.java:482)
at org.apache.sling.models.impl.ModelAdapterFactory.createObject(ModelAdapterFactory.java:560)
... 34 more
For some other Sling models tests the injects work nicely, although for currentPage I am not sure how to proceed. I could not find documentation either about mocking the currentPage object in the Sling model.
Any help would be greatly appreciated.
The below comments helped understand better how this test should be looking. I did some changes, my test is still failing though. Now the classes look like so:
@RunWith(MockitoJUnitRunner.class)
public class MyModelTest {
@Rule
public final AemContext context = new AemContext();
@Mock
private SlingHttpServletRequest request;
@Mock
AemObjectAnnotationProcessorFactory factory;
@InjectMocks
AemObjectInjector aemObjectInjector;
private static final String RESOURCE_PATH = "/content/myproject/jcr:content/mymodel";
private static final String PAGE_PATH = "/content/common/page";
private MyModel mymodel;
@Before
public final void setUp() throws Exception {
context.load().json("/common-page.json", PAGE_PATH);
Resource pageResource = context.resourceResolver().getResource(PAGE_PATH);
Page page = pageResource.adaptTo(Page.class);
context.currentPage(page);
context.load().json("/models/MyModel.json",RESOURCE_PATH);
context.request().setServletPath(RESOURCE_PATH);
context.registerInjectActivateService(factory);
context.registerService(AemObjectInjector.class, aemObjectInjector);
Mockito.when(request.getResource())
.thenReturn(context.resourceResolver().getResource(RESOURCE_PATH));
Resource resource = request.getResource();
mymodel = resource.adaptTo(MyModel.class);
}
@Test
public void simpleLoadTest(){
assertNotNull(mymodel);
}
}
And the updated model with the specific injector:
@Model(
adaptables = { SlingHttpServletRequest.class },
resourceType = MyModel.RESOURCE_TYPE)
public class MyModel {
public static final String RESOURCE_TYPE = "myproject/components/renderer";
@AemObject
private Page currentPage;
// Model methods, etc.
}
The setUp() method does not throw any exception, no warnings whatsoever. The variable mymodel is null so I am still missing things here.
I pushed the code to Github, you can find the project in the following URL https://github.com/josebercianowhitbread/myproject
Notes:
-It was tested in AEM 6.3
-To deploy the project, as usual: mvn clean install -PautoInstallPackage
-The project adds some sample pages to make sure the Sling model works as expected
-The Sling model functionality is quite trivial: it goes up the content tree until it finds the parent node with a "isRootPage" property set to true.
Any questions you might have let me know.
Thanks in advance for any help provided.
Justin Edelson kindly corrected and provided the code of the test. Big thanks to him and also Ahmed Musallam who has chased this post until he made sure everything was working fine :)
The 2 main issues with my initial code were: I was trying to mock the Slick request, but should have used the request from the AemContext instead. The model was not registered.
public class MyModelTest {
@Rule
public final AemContext context = new AemContext();
private MockSlingHttpServletRequest request;
AemObjectAnnotationProcessorFactory factory = new AemObjectAnnotationProcessorFactory();
AemObjectInjector aemObjectInjector = new AemObjectInjector();
private static final String RESOURCE_PATH = "/content/parent-page/jcr:content/content/renderer";
private static final String PAGE_PATH = "/content/parent-page";
private MyModel mymodel;
@Before
public final void setUp() throws Exception {
request = context.request();
context.addModelsForClasses(MyModel.class);
context.load().json("/pages/common-page.json", PAGE_PATH);
Resource pageResource =
context.resourceResolver().getResource(PAGE_PATH);
Page page = pageResource.adaptTo(Page.class);
context.currentPage(page);
context.load().json("/models/MyModel.json", RESOURCE_PATH);
context.registerInjectActivateService(factory);
context.registerService(AemObjectInjector.class, aemObjectInjector);
request.setResource(context.resourceResolver()
.getResource(RESOURCE_PATH));
mymodel = request.adaptTo(MyModel.class);
}
@Test
public void simpleLoadTest() {
assertNotNull(mymodel);
}
}
You are relying on ACS's @AemObject
injector. Remember, that injector, and any sling injector, is an OSGI service, and your AEM context does not have that service registered, i.e: it does not know about the AemObjectInjector and that's why you'll never get a non-null value for Page
.
You'll need to register the injector and the annotation proccessor:
To register the services, take a look at wcm.io's doc: Registering OSGi service
Note: When registering those services, make sure you register a real instance of the services and not a mocked instance. You need the real impl for the sling model injection to occur properly:
aemObjectInjector = new AemObjectInjector()
context.registerService(AemObjectInjector.class, aemObjectInjector);
================================================
UPDATE:
After looking at the simple repo you've provided here I have taken a look and fixed the test to make it work like you want it to in a fork of your repo here
for the sake of everyone else, here are the: model class, test class and json resources:
MyModel.java:
package com.myproject.models;
import javax.annotation.PostConstruct;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.models.annotations.Model;
import com.adobe.acs.commons.models.injectors.annotation.AemObject;
import com.day.cq.wcm.api.Page;
@Model(
adaptables = { SlingHttpServletRequest.class },
resourceType = MyModel.RESOURCE_TYPE)
public class MyModel {
public static final String RESOURCE_TYPE = "myproject/components/renderer";
@AemObject
private Page currentPage;
protected final String ROOT_PAGE_PROPERTY = "isRootPage";
private Page rootPage;
@PostConstruct
private void initModel() {
// Fetches the root language page in order to get the data from that node.
while (!isRootPage(currentPage)) {
currentPage = currentPage.getParent();
}
rootPage = currentPage;
}
private boolean isRootPage(Page selectedPage) {
return selectedPage.getProperties().get(ROOT_PAGE_PROPERTY, false);
}
public String getRootPath() {
return rootPage.getPath();
}
}
Here is the test class: MyModelTest.java
package com.myproject.models;
import static org.junit.Assert.*;
import org.apache.sling.api.SlingHttpServletRequest;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import com.adobe.acs.commons.models.injectors.annotation.impl.AemObjectAnnotationProcessorFactory;
import com.adobe.acs.commons.models.injectors.impl.AemObjectInjector;
import io.wcm.testing.mock.aem.junit.AemContext;
@RunWith(MockitoJUnitRunner.class)
public class MyModelTest {
@Rule
public final AemContext context = new AemContext();
@Mock
private SlingHttpServletRequest request;
private static final String RESOURCE_PATH = "/content/parent-page/jcr:content/content/renderer";
private static final String PAGE_PATH = "/content/parent-page";
private MyModel mymodel;
private AemObjectInjector aemObjectInjector;
private AemObjectAnnotationProcessorFactory factory;
@Before
public final void setUp() throws Exception {
// register model
// NOTE: this is the alternative to creating an adapter/adapter factory.
context.addModelsForClasses(MyModel.class);
// load page and resource from json
context.load().json("/pages/common-page.json", PAGE_PATH);
context.load().json("/models/MyModel.json", RESOURCE_PATH);
// set current page to the page path
context.currentPage(PAGE_PATH);
// register ACS AemObjectInjector service
aemObjectInjector = new AemObjectInjector();
context.registerService(AemObjectInjector.class, aemObjectInjector);
// adapt request to model
mymodel = context.request().adaptTo(MyModel.class);
}
@Test
public void simpleLoadTest() {
// mymodel is NOT null
assertNotNull(mymodel);
// mymodel's page has property 'isRootPage=true', therefor it's the root page
assertEquals(mymodel.getRootPath(), PAGE_PATH);
}
}
The json resources are as follows:
MyModel.json
{
"jcr:primaryType": "nt:unstructured",
"sling:resourceType": "myproject/components/renderer"
}
common-page.json
{
"jcr:primaryType": "cq:Page",
"jcr:createdBy": "admin",
"jcr:created": "Fri Nov 03 2017 13:56:12 GMT+0000",
"jcr:content":
{
"jcr:primaryType": "cq:PageContent",
"jcr:createdBy": "admin",
"jcr:title": "Parent page",
"cq:template": "/apps/myproject/templates/common-page",
"isRootPage": true,
"jcr:created": "Fri Nov 03 2017 13:56:12 GMT+0000",
"cq:lastModified": "Fri Nov 03 2017 13:56:12 GMT+0000",
"sling:resourceType": "myproject/components/page",
"cq:lastModifiedBy": "admin"
}
}