JUnit Test Cases for Sling Models based on Delegation Pattern | Community
Skip to main content
Level 2
December 24, 2019
Solved

JUnit Test Cases for Sling Models based on Delegation Pattern

  • December 24, 2019
  • 12 replies
  • 28399 views

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 @2434638(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

Best answer by Rick_Holdsworth

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>

 

12 replies

arunpatidar
Community Advisor
Community Advisor
December 24, 2019

Hi,

You need to use one more adaptable to adapt Teaser Component 

e.g.

@Model(adaptables = SlingHttpServletRequest.class, adapters = {Teaser.class, CustomTeaser.class}, resourceType = TeaserImpl.RESOURCE_TYPE)

 

Use CustomeTaeser to adapt to Junit test cases.

 

It will not cover 100% coverage but you will be able to write test cases for custom functionality.

Arun Patidar
May 11, 2020

Hi. I am having the same problem here. The core component property is always null in the custom implementation. And making it Optional just skips the instantiation of it.

joerghoh
Adobe Employee
Adobe Employee
December 27, 2019

have you registered the Sling model for the core teaser component as well to the context?

 

Jörg

Level 2
January 2, 2020
Yes. I have included it in the context as follows: context.addModelsForClasses(com.adobe.cq.wcm.core.components.models.Teaser.class);
thienm92844762
January 2, 2020

Thank you so much!

April 7, 2020
Hello! Have you resolved the issue? I mean the case when we use one of the core component Teaser's methods? I also have a NullPointer.
robertod4340573
June 26, 2020

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.

joerghoh
Adobe Employee
Adobe Employee
June 27, 2020

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?

Rick_Holdsworth
Level 2
February 8, 2021
Is there any way you can UN-accept the solution ?
Rick_Holdsworth
Rick_HoldsworthAccepted solution
Level 2
February 9, 2021

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>

 

arunpatidar
Community Advisor
Community Advisor
February 11, 2021
Thanks for sharing
Arun Patidar
Level 2
March 16, 2021
For what it's worth... Do make sure your JSON content includes the sling:resourceSuperType property and value (project/customcomponent). I had extended the Core Text component and had problems because the "coreText" was null, thus could not call getText(), getId(), isRichText(); but once the sling:resourceSuperType was added to the JSON, the tests worked fine and passed.
Level 2
November 15, 2021

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..