I am trying to write jUnit test cases for custom teaser component which extends the core Teaser component using the Delegation Pattern for Sling Models using "@Self @Via(type=ResourceSuperType.class)" as specified here: https://github.com/adobe/aem-core-wcm-components/wiki/Delegation-Pattern-for-Sling-Models
When I try to set the context (io.wcm.testing.mock.aem.junit5.AemContext) and adapt the context's request (I've tried resource as well) to the model I have created (like the "PageHeadline" from the example), I am getting a NullPointerException.
I am using AEM 6.5.2.0 and trying to run test cases in JUnit 5
Solved! Go to Solution.
I've managed to find a solution.
Basically instead of relying on injection during the Unit test I've used Mockito and FieldSetter.setField
The only change to the source code would be to ensure the defaultInjectionStrategy was Optional. Not ideal but minimal
First we need to adapt the request to core Teaser.class
Teaser coreTeaser = request.adaptTo(Teaser.class);
Then we adapt the request to our custom teaser
CustomTeaser underTest = request.adaptTo(CustomTeaser.class);
Then we set the field that's not injecting
try {
FieldSetter.setField(underTest, underTest.getClass().getDeclaredField("teaser"), coreTeaser);
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
Specific to Teaser I also had to set some SlingBindings so that @ScriptVariables were set when adapting the request to Teaser.class
@BeforeEach
request = context.request();
Component component = mock(Component.class);
Style style = mock(Style.class);
SlingBindings slingBindings = (SlingBindings) request.getAttribute(SlingBindings.class.getName());
slingBindings.put(WCMBindingsConstants.NAME_COMPONENT, component);
slingBindings.put(WCMBindingsConstants.NAME_CURRENT_STYLE, style);
request.setAttribute(SlingBindings.class.getName(), slingBindings);
Following may only be specific to our POM setup but thought I'd add for completeness
I also had to bump aem mock junit5 dependency to
<artifactId>io.wcm.testing.aem-mock.junit5</artifactId>
<version>4.0.0</version>
Views
Replies
Total Likes
I have the same issue. I needed to declare the delegate proxy core component (Page in my case) as optional in my sling model as it is not resolved in the unit test (should not be optional in the real use case). Is a pity because my custom logic is simply a fallback of the core component one. I would like to have the object not null in my unit test so I will drop the Optional annotation for the field.
Anyway, next to the working solution this seems to be a short-coming of the mocking libraries. Can you create a minimal testcase (at best without any AEM dependency) and post it to the sling-developer list?
Views
Replies
Total Likes
I've managed to find a solution.
Basically instead of relying on injection during the Unit test I've used Mockito and FieldSetter.setField
The only change to the source code would be to ensure the defaultInjectionStrategy was Optional. Not ideal but minimal
First we need to adapt the request to core Teaser.class
Teaser coreTeaser = request.adaptTo(Teaser.class);
Then we adapt the request to our custom teaser
CustomTeaser underTest = request.adaptTo(CustomTeaser.class);
Then we set the field that's not injecting
try {
FieldSetter.setField(underTest, underTest.getClass().getDeclaredField("teaser"), coreTeaser);
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
Specific to Teaser I also had to set some SlingBindings so that @ScriptVariables were set when adapting the request to Teaser.class
@BeforeEach
request = context.request();
Component component = mock(Component.class);
Style style = mock(Style.class);
SlingBindings slingBindings = (SlingBindings) request.getAttribute(SlingBindings.class.getName());
slingBindings.put(WCMBindingsConstants.NAME_COMPONENT, component);
slingBindings.put(WCMBindingsConstants.NAME_CURRENT_STYLE, style);
request.setAttribute(SlingBindings.class.getName(), slingBindings);
Following may only be specific to our POM setup but thought I'd add for completeness
I also had to bump aem mock junit5 dependency to
<artifactId>io.wcm.testing.aem-mock.junit5</artifactId>
<version>4.0.0</version>
Views
Replies
Total Likes
Views
Replies
Total Likes
Hi, I have the following problem: when I try to access the delegate object from the test class, it results null. Below is the code snippet:
java model:
@Model(adaptables = SlingHttpServletRequest.class, adapters = {Button.class, ComponentExporter.class}, resourceType = MYPROJCTAModel.RESOURCE_TYPE, defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
public class MYPROJCTAModel implements Button {
public static final String RESOURCE_TYPE = "myprog/components/MYPROJ-CTA";
private LinkItem li;
private final Logger logger = LoggerFactory.getLogger(getClass());
@Deleted Account
protected SlingHttpServletRequest request;
@ScriptVariable
private PageManager pageManager;
@ValueMapValue(injectionStrategy = InjectionStrategy.OPTIONAL)
@nullable
private String link;
@Deleted Account @Via(type = ResourceSuperType.class)
private Button delegate;
@inject
@Deleted Account
private String prefix;
@PostConstruct
protected void initModel() {
try {
li = CommonUtils.getLinkTracking(prefix, request, link, pageManager);
String tmpLinkText = delegate.getText();
if (StringUtils.isNotBlank(tmpLinkText)) {
li.setText(tmpLinkText);
}
} catch (Exception e) {
logger.error("Error on initModel: ", e);
}
}
Junit class:
@ExtendWith({AemContextExtension.class, MockitoExtension.class})
class MYPROJCTAModelTest {
private final AemContext ctx = new AemContext();
@Mock
private Button button;
@Mock
private ModelFactory modelFactory;
private static final String FILE_JSON_TEST = "/com/myproj/it/cmp/core/models/cta.json";
@BeforeEach
void setUp() throws Exception {
ctx.addModelsForClasses(MYPROJCTAModel.class);
ctx.load().json(FILE_JSON_TEST, "/cta");
lenient().when(modelFactory.getModelFromWrappedRequest(eq(ctx.request()),
any(Resource.class),
eq(Button.class))).thenReturn(button);
ctx.registerService(ModelFactory.class, modelFactory,
org.osgi.framework.Constants.SERVICE_RANKING, Integer.MAX_VALUE);
Component component = Mockito.mock(Component.class);
Style style = Mockito.mock(Style.class);
SlingBindings slingBinding = (SlingBindings) ctx.request().getAttribute(SlingBindings.class.getName());
slingBinding.put(WCMBindingsConstants.NAME_COMPONENT, component);
slingBinding.put(WCMBindingsConstants.NAME_CURRENT_STYLE, style);
ctx.request().setAttribute(SlingBindings.class.getName(), slingBinding);
}
@test
public void testLinkItem() {
ctx.currentResource("/cta" + "/jcr:content/root/container" + "/cta");
MYPROJCTAModel customModel = ctx.request().adaptTo(MYPROJCTAModel.class);
LinkItem li = customTitle.getLinkItem();
assertTrue(li != null);
}
}
Views
Replies
Total Likes
Fieldsetter is now deprecated and the class hence not usable anymore.
How do we set the field that's not injecting in that case ?
@Rohan_Garg I facing same issue. Did you find the solution?
Views
Replies
Total Likes
I managed to get a mock instance to be injected when adapting the commerce product model without using the "DefaultInjectionStrategy.OPTIONAL" solution.
I've just wrapped a mock to avoid all of the inject errors of the core model but it did try to do it with the original resource super type.
package com.custom.aem.core.models.commerce.impl; import com.adobe.cq.commerce.core.components.models.common.Price; import com.adobe.cq.commerce.core.components.models.product.Asset; import com.adobe.cq.commerce.core.components.models.product.GroupItem; import com.adobe.cq.commerce.core.components.models.product.Variant; import com.adobe.cq.commerce.core.components.models.product.VariantAttribute; import com.adobe.cq.commerce.core.components.models.retriever.AbstractProductRetriever; import com.adobe.cq.commerce.core.components.storefrontcontext.ProductStorefrontContext; import com.custom.aem.core.models.commerce.Product; import io.wcm.testing.mock.aem.junit5.AemContext; import io.wcm.testing.mock.aem.junit5.AemContextExtension; import junitx.util.PrivateAccessor; import org.apache.sling.api.SlingHttpServletRequest; import org.apache.sling.models.annotations.Model; import org.apache.sling.models.factory.ModelFactory; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mockito; import java.util.List; import static org.junit.jupiter.api.Assertions.assertNotNull; /** * Simple JUnit test verifying the HelloWorldModel */ @ExtendWith(AemContextExtension.class) class ProductImplTest2 { private Product model; private ModelFactory modelFactory; private com.adobe.cq.commerce.core.components.models.product.Product delegateProductMock; private AbstractProductRetriever productRetriever; @BeforeEach public void setup(final AemContext context) throws Exception { context.addModelsForClasses( ProductImpl.class, Product.class, DelegateProductModel.class, com.adobe.cq.commerce.core.components.models.product.Product.class ); context.build() .resource("/content/custom-aem/us/en/products/product-page/product", "sling:resourceType", "custom-aem/components/commerce/product") .resource("/apps/custom-aem/components/commerce/product", "sling:resourceSuperType", "core/cif/components/commerce/product/v1/productMock") .commit(); context.currentResource("/content/custom-aem/us/en/products/product-page/product"); this.modelFactory = context.getService(ModelFactory.class); final com.adobe.cq.commerce.core.components.models.product.Product coreProduct = Mockito.mock(com.adobe.cq.commerce.core.components.models.product.Product.class); this.model = this.modelFactory.createModel(context.request(), ProductImpl.class); final DelegateProductModel delegateProductModel = (DelegateProductModel) PrivateAccessor.getField(this.model, "product"); this.delegateProductMock = delegateProductModel.delegateProductMock; this.productRetriever = delegateProductModel.productRetriever; } @Test void test(final AemContext context) throws Exception { assertNotNull(this.model); } /** * Mock Class to wrap a mock that can be injected into the class under test via the adapt mechanism. */ @Model( adaptables = {SlingHttpServletRequest.class}, adapters = {com.adobe.cq.commerce.core.components.models.product.Product.class}, resourceType = {"core/cif/components/commerce/product/v1/productMock"}, cache = true ) public static class DelegateProductModel implements com.adobe.cq.commerce.core.components.models.product.Product{ com.adobe.cq.commerce.core.components.models.product.Product delegateProductMock = Mockito.mock(com.adobe.cq.commerce.core.components.models.product.Product.class); private final AbstractProductRetriever productRetriever = Mockito.mock(AbstractProductRetriever.class); @Override public Boolean getFound() { return this.delegateProductMock.getFound(); } @Override public String getName() { return this.delegateProductMock.getName(); } @Override public String getDescription() { return this.delegateProductMock.getDescription(); } @Override public String getSku() { return this.delegateProductMock.getSku(); } @Override public Price getPriceRange() { return this.delegateProductMock.getPriceRange(); } @Override public Boolean getInStock() { return this.delegateProductMock.getInStock(); } @Override public Boolean isConfigurable() { return this.delegateProductMock.isConfigurable(); } @Override public Boolean isGroupedProduct() { return this.delegateProductMock.isGroupedProduct(); } @Override public Boolean isVirtualProduct() { return this.delegateProductMock.isVirtualProduct(); } @Override public Boolean isBundleProduct() { return this.delegateProductMock.isBundleProduct(); } @Override public String getVariantsJson() { return this.delegateProductMock.getVariantsJson(); } @Override public List<Variant> getVariants() { return this.delegateProductMock.getVariants(); } @Override public List<GroupItem> getGroupedProductItems() { return this.delegateProductMock.getGroupedProductItems(); } @Override public List<Asset> getAssets() { return this.delegateProductMock.getAssets(); } @Override public String getAssetsJson() { return this.delegateProductMock.getAssetsJson(); } @Override public List<VariantAttribute> getVariantAttributes() { return this.delegateProductMock.getVariantAttributes(); } @Override public Boolean loadClientPrice() { return this.delegateProductMock.loadClientPrice(); } @Override public AbstractProductRetriever getProductRetriever() { return this.productRetriever; } @Override public ProductStorefrontContext getStorefrontContext() { return this.delegateProductMock.getStorefrontContext(); } @Override public String getMetaDescription() { return this.delegateProductMock.getMetaDescription(); } @Override public String getMetaKeywords() { return this.delegateProductMock.getMetaKeywords(); } @Override public String getMetaTitle() { return this.delegateProductMock.getMetaTitle(); } @Override public String getCanonicalUrl() { return this.delegateProductMock.getCanonicalUrl(); } }; }
It's quite verbose, may be there's a cleaner solution..
Views
Replies
Total Likes
Adding a better and cleaner solution for those who are having this issue. wcm.io provides libraries and extension for AEM applications. We can use the same to create AemContext.
They have recently addresses this issue and added straightforward solution (issue link), details here. We can import
com.adobe.cq.wcm.core.components.testing.mock.ContextPlugins.CORE_COMPONENTS and use them as a parameter to .plugin(). So our AemContext initialization would look like this.
private final AemContext ctx = new AemContextBuilder(ResourceResolverType.JCR_MOCK).plugin(CORE_COMPONENTS).build();
NOTE: ResourceResolverType.JCR_MOCK parameter is just for having extra features in my test case, that can be ignored too.
I used this plugin to implement test cases for custom list component that extends list component and uses delegate pattern for most of methods but uses some custom methods that uses delegated methods inside them. I didn't even have to adapt List model into AemContext. It didn't even require to mock anything for getting delegated patterns to work.
Thanks for pointing me in the right direction. However I am still unable to make this work. None of the core components properties are being injected, I can only see the custom components properties. Below is my minimal unit test case. Would really appreciate any help here.
@ExtendWith(AemContextExtension.class)
public class FormTextModelTest {
private final AemContext context = new AemContextBuilder(ResourceResolverType.JCR_MOCK)
.plugin(CORE_COMPONENTS)
.build();
private FormTextModel formTextModel;
@BeforeEach
void setUp() {
context.create().resource("/apps/myproject/components/content/coreform/text",
"sling:resourceSuperType", "core/wcm/components/button/v2/button");
Page page = context.create().page("/content/test-page");
context.currentResource(context.create().resource(page, "extendedText",
"sling:resourceType", "myproject/components/content/coreform/text",
"required", "true",
"requiredMessage","Required message when required checkbox is true"
));
formTextModel = context.request().adaptTo(FormTextModel.class);
}
@Test
void testGetRequiredMessageWhenMsgIsAuthoredAndRequiredCheckBoxIsTrue() {
String expected = "Required message when required checkbox is true";
String actual = formTextModel.getRequiredMessage();
assertEquals(expected, actual);
}
}
Views
Replies
Total Likes
The logic is simple, we need to get sling:resourceSuperType available at run time to insect Teaser in Sling model.
context.create().resource("/apps/project/components/core/teaser","sling:resourceSuperType", "core/wcm/components/teaser/v2/teaser");
@BeforeEach looks like below:
@BeforeEach
public final void setUp() {
context.create().resource("/apps/project/components/core/teaser","sling:resourceSuperType", "core/wcm/components/teaser/v2/teaser");
context.load().json("/teaser.json", "/content/site/teaser");
teaserComponent = Objects.requireNonNull(context.currentResource("/content/site/teaser")).adaptTo(TeaserComponent.class);
}
And Test looks like below:
//@Test
void test() {
assertNotNull(teaserComponent);
assertNotNull(teaserComponent.getTeaser());
assertEquals("custom-field-value", teaserComponent.getCustomField());
}
TeaserComponentTest:
@ExtendWith(AemContextExtension.class)
public class TeaserComponentTest {
private final AemContext context = AppAemContext.newAemContext();
private TeaserComponent teaserComponent;
/**
* Setup method
*/
@BeforeEach
public final void setUp() {
context.create().resource("/apps/project/components/core/teaser","sling:resourceSuperType", "core/wcm/components/teaser/v2/teaser");
context.load().json("/teaser.json", "/content/site/teaser");
teaserComponent = Objects.requireNonNull(context.currentResource("/content/site/teaser")).adaptTo(TeaserComponent.class);
}
//@Test
void test() {
assertNotNull(teaserComponent);
assertNotNull(teaserComponent.getTeaser());
assertEquals("custom-field-value", teaserComponent.getCustomField());
}
}
Views
Replies
Total Likes