Expand my Community achievements bar.

Dive into Adobe Summit 2024! Explore curated list of AEM sessions & labs, register, connect with experts, ask questions, engage, and share insights. Don't miss the excitement.
SOLVED

AEM @Model newbie - junit tests - @Model doesn't adapt to fake "Resource"

Avatar

Level 7
hi folks,
I'm trying to get into using @Models instead of Use classes and Servlets.
I can get the @Models to work o.k. but I'm having trouble with unit tests for them.
I set up a pretend "Resource" with a node with a string property "target" of value "target".
However, I can't map my @Model to it. I just get back "nulls" instead of parameter values.
See Model and Unit Test below.
Any ideas ?

thanks
Fiona

@Model(
// This must adapt from a SlingHttpServletRequest, since this is invoked directly via a request, and not via a resource.
// If can specify Resource.class as a second adaptable as needed
adaptables = { SlingHttpServletRequest.class, Resource.class },
adapters = {RelatedArticleModel.class},
// The resourceType is required if you want Sling to "naturally" expose this models as the exporter for a Resource.
resourceType = RelatedArticleModel.TITLE_RESOURCE_TYPE,
defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL
)
// name = the registered name of the exporter
// extensions = the extensions this exporter is registered to
// selector = defaults to "models", can override as needed; This is helpful if a single resource needs 2 different JSON renditions
@Exporter(name = "jackson", extensions = "json", options = {
/**
* Jackson options:
* - Mapper Features: http://static.javadoc.io/com.fasterxml.jackson.core/jackson-databind/2.8.5/com/fasterxml/jackson/databind/MapperFeature.html
* - Serialization Features: http://static.javadoc.io/com.fasterxml.jackson.core/jackson-databind/2.8.5/com/fasterxml/jackson/databind/SerializationFeature.html
*/
@ExporterOption(name = "MapperFeature.SORT_PROPERTIES_ALPHABETICALLY", value = "true"),
@ExporterOption(name = "SerializationFeature.WRITE_DATES_AS_TIMESTAMPS", value="false")
})

public class RelatedArticleModel {

public static final String TITLE_RESOURCE_TYPE = "xyz/components/content/relatedarticle";
@Self
private SlingHttpServletRequest request;

@ScriptVariable
private ResourceResolver resolver;

@ValueMapValue(optional = false)
private String title;

@ValueMapValue(optional = false)
private String target;

@PostConstruct
// PostConstructs are called after all the injection has occurred, but before the Model object is returned for use.
private void init() {
// do stuff
}

public String getTitle() {
return title;
}

public String getTarget() {
return target;
}

public void setTitle(String title) {
this.title = title;
}

public void setTarget(String target) {
this.target = target;
}

}

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

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

public final AemContext ctx = new AemContext(ResourceResolverType.JCR_MOCK);

private RelatedArticleModel underTest;

@BeforeEach
void setUp() throws Exception {
ctx.addModelsForClasses(RelatedArticleModel.class);
}

@Test
public void testGetTarget() {
Resource relatedArticleResourceContext = ctx.create().resource("/content/relatedarticle",
"sling:resourceType", "xyz/components/content/relatedarticle",
"target", "target");

System.out.println("resource path is " + relatedArticleResourceContext.getPath());
System.out.println("resource type is " + relatedArticleResourceContext.getResourceType());
System.out.println("resource name is " + relatedArticleResourceContext.getName());
System.out.println("resource target from valuemap is " + relatedArticleResourceContext.getValueMap().get("target", String.class));

underTest = relatedArticleResourceContext.adaptTo(RelatedArticleModel.class);

assertEquals("target", underTest.getTarget()); <<< *****Null pointer exception, underTest.getTarget() is null*****


}

}




1 Accepted Solution

Avatar

Correct answer by
Community Advisor

Hi @fionas76543059,

Try to create resource properties as Map object as shown in the official doc. (based on the way you are creating mock resource).

Alternatively you can also try to create mock resource with properties as JSON file and use context.load.json("json path", "sample resource path") - Sample snippet available in the same doc.

 

https://wcm.io/testing/aem-mock/usage-content-loader-builder.html

Vijayalakshmi_S_0-1627493481338.png

 

View solution in original post

9 Replies

Avatar

Correct answer by
Community Advisor

Hi @fionas76543059,

Try to create resource properties as Map object as shown in the official doc. (based on the way you are creating mock resource).

Alternatively you can also try to create mock resource with properties as JSON file and use context.load.json("json path", "sample resource path") - Sample snippet available in the same doc.

 

https://wcm.io/testing/aem-mock/usage-content-loader-builder.html

Vijayalakshmi_S_0-1627493481338.png

 

Avatar

Level 7
Thank you. That was interesting info. Following the example I was able to have a Page resource in a JSON file and adaptTo it to Page.class. However, when I try adaptTo() one of my own @Model classes, I just get a null object, even when I have a Adaptables annotation of Resource.class on the @Model.

Avatar

Community Advisor

Hi @fionas76543059,

Did you try the below and still getting NPE ?

We need to create a mock Resource object with properties as your model's sling:resourceType and other properties you are injecting in your model. In this example, it is "target" and "title" 

Resource mockResc = context.create().resource("/content/test1", ImmutableMap.<String, Object>builder()
        .put("sling:resourceType", "xyz/components/content/relatedarticle")
        .put("target", "target").put("title", "Article")
        .build());
mockResc.adaptTo(RelatedArticleModel.class);

 

Avatar

Community Advisor

@fionas76543059

Possible causes of mock Resource not adapting to your model

  • All the mandatory properties that are injected should be available in the mock Resource. 
  • If you have any logic in @PostConstruct method and if it is not visible in the AemContext object, then it will prevent the Model Instantiation (that is adapting mock resource to your Model.class)
    • Example : If we use UserManager API in the model, then the same needs to be set in the AemContext as Mock Object before we could instantiate the Model. (it is like preparing the AemContext with what we use in our Model. Otherwise that API will be null and hence the ultimate model object is null)

Avatar

Community Advisor

Hi @Vijayalakshmi_S 

 

I am stuck in same scenario, where I have @PostConstruct method in model and I am initializing resourceResolver in that. This is working fine on actual page.

 @PostConstruct
    public void init(){
        resourceResolver = page.getContentResource().getResourceResolver();
    }

But while writing Junit for that particular Sling Model, It's giving NPE because of this @PostConstruct method.

Can you help me on this? How to mock this resolver thing in Junits?

 

Thanks

Avatar

Community Advisor

@iamnjain 

Cross check if the "page" Object(that you are using to get the resolver) is set/visible in the AemContext.

Alternatively you can inject ResourceResolver via @ScriptVariable Annotation in your actual Sling Model Class. 

 

 

 

Avatar

Level 7

Thanks!

I tried with a much simpler @Model that only has 2 parameters and no PostConstruct stuff.. The resource is read fine from the file.but after the adaptTo, I just get a Null Pointer exception when I try to access the class parameters.

 

{
"cta": {
"jcr:primaryType": "nt:unstructured",
"sling:resourceType": "xya/components/content/cta",
"target": "target",
"icon": "image"
}
}

 

@Model(adaptables = Resource.class)

public class CallToActionModel {

   private String icon;

   private String target;

 

   public String getTarget() { return target; }

   public void setTarget(String target) { this.target = target; }

   public String getIcon() { return icon; }

   public void setIcon(String icon) { this.icon = icon; }

}

 

 

        context.load().json("/CTAModelTest.json", "/content");
Resource resource = context.resourceResolver().getResource("/content/cta");
System.out.println("resource path is " + resource.getPath());
System.out.println("resource type is " + resource.getResourceType());

CallToActionModel cta = resource.adaptTo(CallToActionModel.class);
System.out.println("cta is " + cta.toString());

 

Avatar

Community Advisor

@fionas76543059,

Small change in the JSON content and path. Can you try the below and let know if it works. 

JSON file content :

{
    "jcr:primaryType": "nt:unstructured",
    "sling:resourceType": "xya/components/content/cta",
    "target": "target",
    "icon": "image"
}

Code snippet: (Path to which we load the JSON and the path that we set the context with is same)

private final String CONTENT_PATH = "/content/cta";
private final String MOCK_RESC_JSON = "/CTAModelTest.json"; (this json is available under src/test/resources)
aemContext.load().json(MOCK_RESC_JSON, CONTENT_PATH);
Resource resource = aemContext.currentResource(CONTENT_PATH);
CallToActionModel cta = resource.adaptTo(CallToActionModel.class);

 

Avatar

Level 7

Thanks Vijayalakschmi!

That seem to do the trick!

 

 

I also added

context.addModelsForClasses(CallToActionModel.class);

 

and I put @ValueMapValue() annotations on the parameters in the model also.