Expand my Community achievements bar.

SOLVED

AEM - JUnit @Inject annotation

Avatar

Level 2

Hi all,
I’am facing an issue regarding the handling of the @inject annotation with AEM-Junit test scenario.
Here an example of the issue:
Model Class:

 

@Model(adaptables = {SlingHttpServletRequest.class}, defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
public class SimpleModel  {

    @inject
    private Resource currentResource;

    public Resource getCurrentResource(){
       return currentResource;
    }

}

 

Test Class:

 

@ExtendWith({AemContextExtension.class, MockitoExtension.class})
public class SimpleModelTest{

    private AemContext context = new AemContext();
    private SimpleModel simpleModel;

    @BeforeEach
    public void setup() throws Exception {
        context.addModelsForClasses(SimpleModel.class);
        context.load().json("SimpleModelTest.json", "/content/myproject/us");
    }

    @test
    void testResource() {
        context.currentResource("/content/myproject/us/jcr:content/simple-component");
        simpleModel = context.request().adaptTo(SimpleModel.class);
        assertNotNull(simpleModel.getCurrentResource());
    }

}

 

The test fails since simpleModel.getCurrentResource() gives me a null resource.
If I change the @inject annotation in the SimpleModel class with @SlingObject the test doesn't fail, but I don’t want to change every annotation with SlingObject or the similar one.

Could you help me to solve this issue?

1 Accepted Solution

Avatar

Correct answer by
Employee Advisor

My thinking is that it should not need these statements. Also a page injector is not default (IIRC it is a wcm.io feature).

 

Have you tried to switch to this:

@Model(adaptables = {Resource.class}, defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
public class SimpleModel  {
...
}

If you don't need any information from the request (e.g. a parameter) this is much more flexible.

View solution in original post

23 Replies

Avatar

Employee Advisor

Hi @federicov798870 ,

 

There were multiple errors , I am pasting the working code here :

SimpleModel.class:

import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.models.annotations.DefaultInjectionStrategy;
import org.apache.sling.models.annotations.Model;

import javax.inject.Inject;

@Model(adaptables = {SlingHttpServletRequest.class}, defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
public class SimpleModel  {

    @Inject
    public Resource currentResource;

    public Resource getCurrentResource(){
        return currentResource;
    }

}
SimpleModelTest.class

import io.wcm.testing.mock.aem.junit5.AemContext;
import io.wcm.testing.mock.aem.junit5.AemContextExtension;
import junit.framework.Assert;
import org.apache.sling.api.resource.Resource;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;

import static junit.framework.Assert.assertNotNull;

@ExtendWith({AemContextExtension.class, MockitoExtension.class})
public class SimpleModelTest{

    private AemContext context = new AemContext();
    private SimpleModel simpleModel;

    @BeforeEach
    public void setup() throws Exception {
        context.addModelsForClasses(SimpleModel.class);
        context.load().json("/com/aem/geeks/core/models/impl/Author.json", "/content/myproject/us");
    }

    @Test
    void testResource() {
        Resource resource = context.currentResource("/content/myproject/us/jcr:content/simple-component");
        simpleModel = context.request().adaptTo(SimpleModel.class);
        simpleModel.currentResource = resource;        Assert.assertNotNull(simpleModel.getCurrentResource());
    }

}
Author.json
{
  "jcr:content":{
    "jcr:primaryType":"cq:PageContent",
    "jcr:createdBy":"admin",
    "jcr:title":"Author Bio",
    "cq:contextHubSegmentsPath":"/etc/segmentation/contexthub",
    "jcr:created":"Tue Oct 27 2020 22:11:34 GMT+0530",
    "cq:lastModified":"Mon Nov 02 2020 23:25:43 GMT+0530",
    "cq:contextHubPath":"/etc/cloudsettings/default/contexthub",
    "cq:lastModifiedBy":"admin",
    "simple-component": {
      "random": "test"
    }
  }
}

I have highlisted the mistakes in bold, also for junits you need to command the code what to pickup, in your case you were not commanding the model which resource to be picked up which is why it always returned null.

 

Reference : https://www.youtube.com/watch?v=g5x6F8bUHj8

 

Thanks.

Avatar

Level 2

Hi @milind_bachani ,
thank you for your reply.
I'am looking for a solution in which I don't need to perform resource setter by my hand.
If you take the same code that you have posted, and you remove this line:
"simpleModel.currentResource = resource;" and change the @Inject annotation with @SlingObject annotation in the SlingModel, the adaptTo works without the need to set resource.
Is it possibile for you to achive the same result with @Inject annotation?
Thank you again for your reply,
Federico


Avatar

Employee Advisor

@federicov798870 Ah okay, I understood what's the ask - however, is there any particular reason you dont want to set the object manually ?

Avatar

Level 2

@milind_bachaniThe main reason is that I don't want to change / add code to all my Sling Models in my projects (more or less they are 300) for test implementation.
Adding setter and hook to call @PostCostructor method require time and effort (setting all to public I'dont think is a best practice solution, but I'dont know).
For this reason, I am wondering if there is a "fast" way to deal with @Inject annotations.

Avatar

Employee Advisor

In that case you can use when, thenReturn using mocks:

@Mock
Resource mockresource
when(simpleModel.getCurrentResource()).thenReturn(mockresource);

Avatar

Level 2

@milind_bachani ok, but with a postConstruct method I should rewirte the model code, right?
For istance, with a model like this, when method cannot be used right?

@Model(adaptables = {SlingHttpServletRequest.class}, defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
public class SimpleModel  {

    @Inject
    private Resource currentResource;
    
@Getter
private String childName;

@PostConstruct
public init() {
Resource child = currentResource.getChild("child");
if(child != null) {
childName = child.getName();
}
} }

 Thank you again for your time and support.

Avatar

Employee Advisor

@federicov798870 

In that case for Junit :

when(simpleModel.getCurrentResource()).thenReturn(mockresource);
when(mockresource.getChild(anyString())).thenReturn(mockresource);

You can create 2 test cases as:
1. resourceHasChild() with above code

2. resouceHasNoChild() remove second line from above snippet.

 

This will also create a precise test suite, thanks.

Avatar

Level 2

@milind_bachani thank you, but this line: 

Resource child = currentResource.getChild("child");

gives me a nullpointer because currentResource is null.

Avatar

Employee Advisor

@federicov798870 

Please add:

@BeforeEach
    public void setup() throws Exception {
        MockitoAnnotations.initMocks(this);
}

in test class and it should work, working for me - if you need I can post the whole code.

 

thanks.

Avatar

Level 2

@milind_bachani If you can post the whole code, I would appreciate it very much.
Thank you again for your support.

Avatar

Employee Advisor

Hi @federicov798870 , sorry for the delayed response as I was away, here you go :

package com.aem.geeks.core.models.impl;


import io.wcm.testing.mock.aem.junit5.AemContext;
import io.wcm.testing.mock.aem.junit5.AemContextExtension;
import junit.framework.Assert;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.models.spi.injectorspecific.InjectAnnotation;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.mockito.junit.MockitoJUnitRunner;
import org.mockito.junit.jupiter.MockitoExtension;

import javax.inject.Inject;

import static junit.framework.Assert.assertNotNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;


@RunWith(MockitoJUnitRunner.class)
public class SimpleModelTest{

    private final AemContext aemContext=new AemContext();

    @InjectMocks
    private SimpleModel simpleModel;
    @Mock
    private Resource mockresource;

    @BeforeEach
    public void setup() throws Exception {
        MockitoAnnotations.initMocks(this);
        simpleModel = mock(SimpleModel.class);
    }

    @Test
    void testResource() {
        when(simpleModel.getCurrentResource()).thenReturn(mockresource);
        assertNotNull(simpleModel.getCurrentResource());
    }

}

Avatar

Level 2

Hi @milind_bachani,
thank you again for your reply.
By using this approch, the @PostCostruct method is not executed automatically by using the mocked currentResource.

For istance:

@Model(adaptables = {SlingHttpServletRequest.class}, defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
public class SimpleModel  {

    @Inject
    private Resource currentResource;
    
@Getter
private String childName;

@PostConstruct
public init() {
Resource child = currentResource.getChild("child");
if(child != null) {
childName = child.getName();
}
} }

How can I test the childName value if it is correcly computed to my model?
Even If I call it by my hand I don't obtain the result:

simpleModel.init(); 
assertNotNull(simpleModel.getChildName());

Thank you for your time!

Avatar

Employee Advisor

Here you go :

package com.aem.geeks.core.models.impl;

import jdk.nashorn.internal.objects.annotations.Getter;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.models.annotations.DefaultInjectionStrategy;
import org.apache.sling.models.annotations.Model;

import javax.annotation.PostConstruct;
import javax.inject.Inject;

@Model(adaptables = {SlingHttpServletRequest.class}, defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
public class SimpleModel  {

    @Inject
    protected Resource currentResource;

    String childName;

    String getChildName(){
        return childName;
    }

    @PostConstruct
    protected void init() {
        Resource child = currentResource.getChild("child");
        if(child != null) {
            childName = child.getName();
        }
    }

}
Test

import io.wcm.testing.mock.aem.junit5.AemContext;
import org.apache.sling.api.resource.Resource;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.junit.MockitoJUnitRunner;

import static junit.framework.Assert.assertEquals;
import static org.mockito.Mockito.when;


@RunWith(MockitoJUnitRunner.class)
public class SimpleModelTest{

    private final AemContext aemContext=new AemContext();

    @InjectMocks
    private SimpleModel simpleModel;
    @Mock
    private Resource mockresource;

    @BeforeEach
    public void setup() throws Exception {
        MockitoAnnotations.initMocks(this);
    }

    @Test
    void testResource() {
        simpleModel.currentResource = mockresource;
        when(simpleModel.currentResource.getChild("child")).thenReturn(mockresource);
        when(mockresource.getName()).thenReturn("test");
        simpleModel.init();
        assertEquals("test", simpleModel.getChildName());
    }

}

Screenshot 2022-02-11 at 6.07.34 PM.png

Avatar

Level 2

I understood how the test cases should be implemented.
Thank you again @milind_bachani

Avatar

Employee Advisor

I don't like this approach, because you mock a lot of stuff, while the AEM Mocks can do the majority for you already.

Avatar

Level 2

Hi @Jörg_Hoh ,

thank you, which approach do you suggest to follow to deal with the @inject annotation?

Thank you again for your support.

Avatar

Employee Advisor

Technically your code looks correct, and I am also not aware of a problem or missing piece of functiionality in that area.

 

What looks strange is that you can only adapt from a request to the SlingModel (I would change it to make it adaptable from a Resource (only), because this gives you more flexibility to use this model); but then you just set the current resource to that path.

 

have you tried already something like this:

    @test
    void testResource() {
        Resource r = context.resourceResolver.getResource("/content/myproject/us/jcr:content/simple-component");
        simpleModel = r.adaptTo(SimpleModel.class);
        assertNotNull(simpleModel.getCurrentResource());
    }

 

Avatar

Level 2

Hi @Jörg_Hoh ,

yes, I tried that. I think that the problem was the @Inject annotation. For istance, if you have a model which Inject the currentPage with the @Inject annotation, the code that you posted doesn't work since the currentPage is null, but if you change the @Inject annotation in your model with another one (e.g. @ScriptVariable) it works. Same result with the other annotations (@SlingObject etc).

Thank you for your time and support.

Avatar

Employee Advisor

That means, that just the "@inject" annotation does not work in the tests?

 

Regarding this I found https://medium.com/ida-mediafoundry/sling-models-the-story-behind-inject-c51615164b11 which explains a bit of the magic behind this annotation. 

 

I normally use the other annotations to explicitly express which injector should be used, so I don't have experience with "@inject" in unittests.

Avatar

Level 2

Hi @Jörg_Hoh ,
thank you for your suggestion. Here what I have obtained:

Model

import lombok.Getter; import org.apache.sling.api.SlingHttpServletRequest; import org.apache.sling.api.resource.Resource;
import com.day.cq.wcm.api.Page; import org.apache.sling.models.annotations.DefaultInjectionStrategy; import org.apache.sling.models.annotations.Model; import javax.annotation.PostConstruct; import javax.inject.Inject; @Model(adaptables = {SlingHttpServletRequest.class}, defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL) public class SimpleModel { @Inject private Resource currentResource;

@Inject
private Page currentPage;
@Getter private String resourceName;

@Getter
private String pagePath;
@PostConstruct protected void init() { resourceName = currentResource.getName();
pagePath = currentPage.getPath(); } }

 

Test
import io.wcm.testing.mock.aem.junit5.AemContext;
import org.apache.sling.api.resource.Resource;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.junit.MockitoJUnitRunner;
import com.day.cq.wcm.api.Page;
import io.wcm.testing.mock.aem.junit5.AemContextExtension; import org.mockito.junit.jupiter.MockitoExtension; import static junit.framework.Assert.assertEquals; import static org.mockito.Mockito.when; @ExtendWith({AemContextExtension.class, MockitoExtension.class})
@RunWith(MockitoJUnitRunner.class) public class SimpleModelTest { private final AemContext aemContext=new AemContext(); @BeforeEach public void setup() throws Exception { context.load().json("SimpleModelTest.json", "/content/page");
context.registerService(Resource.class, context.currentResource("/content/page/jcr:content/mycomponent"));
context.registerService(Page.class, context.currentPage("/content/page"));
} @Test void testResource() {
SimpleModel simpleModel = context.request().adaptTo(SimpleModel.class); assertEquals("/content/page", simpleModel.getPagePath());
assertEquals("mycomponent", simpleModel.getResourceName()); } }

 

 

The registerService allows @Inject annotation to inject Resource and Page by finding the right Injector.
Of course this need to be done for every "types" of inject (resourceResolver etc), but seems to be more elegant respect to the "setter and mocker" approach.
What do you think about this solution?


Thank you for your time and support!