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?
Solved! Go to Solution.
Views
Replies
Total Likes
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.
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.
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
@federicov798870 Ah okay, I understood what's the ask - however, is there any particular reason you dont want to set the object manually ?
@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.
In that case you can use when, thenReturn using mocks:
@Mock Resource mockresource
when(simpleModel.getCurrentResource()).thenReturn(mockresource);
@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.
@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.
@milind_bachani thank you, but this line:
Resource child = currentResource.getChild("child");
gives me a nullpointer because currentResource is null.
@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.
@milind_bachani If you can post the whole code, I would appreciate it very much.
Thank you again for your support.
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()); } }
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!
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()); } }
I understood how the test cases should be implemented.
Thank you again @milind_bachani
Views
Replies
Total Likes
I don't like this approach, because you mock a lot of stuff, while the AEM Mocks can do the majority for you already.
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()); }
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.
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.
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!