Sling model unit tests: mock currentPage in SlingHttpServletRequest

Jose Berciano picture Jose Berciano · Oct 28, 2017 · Viewed 9.3k times · Source

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.


UPDATE

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.


UPDATE 2

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.


UPDATE 3

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);
   }

}

Answer

Ahmed Musallam picture Ahmed Musallam · Oct 30, 2017

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"
    }
}