Expand my Community achievements bar.

Enhance your AEM Assets & Boost Your Development: [AEM Gems | June 19, 2024] Improving the Developer Experience with New APIs and Events

How to write Unit Tests for Extended Core Components' Java Models

Avatar

Level 4

I extended the Text Core Component's Java Class with a simple "moreText" variable. 

 

But I am still using some of the Core Component's methods in the HTL so I need them in my Java Class. 

 

How can I Unit Test the Extended Core Component's methods that require the ResourceSuperType?

 

package com.adobe.aem.guides.wknd.core.models;

import com.adobe.cq.export.json.ComponentExporter;
import com.adobe.cq.export.json.ExporterConstants;
import com.adobe.cq.wcm.core.components.models.Text;
import lombok.Getter;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.models.annotations.Default;
import org.apache.sling.models.annotations.DefaultInjectionStrategy;
import org.apache.sling.models.annotations.Exporter;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.Via;
import org.apache.sling.models.annotations.injectorspecific.Self;
import org.apache.sling.models.annotations.injectorspecific.ValueMapValue;
import org.apache.sling.models.annotations.via.ResourceSuperType;

@Model(
        adaptables = SlingHttpServletRequest.class,
        adapters = {ComponentExporter.class},
        resourceType = ExtendedCoreComponentModel.RESOURCE_TYPE,
        defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL
)
@Exporter(
        name = ExporterConstants.SLING_MODEL_EXPORTER_NAME,
        extensions = ExporterConstants.SLING_MODEL_EXTENSION
)
public class ExtendedCoreComponentModel implements Text {

    static final String RESOURCE_TYPE = "wknd/components/extendedcorecomponent";

    @Self
    @Via(type = ResourceSuperType.class)
    protected Text text;

    @getter
    @ValueMapValue
    @default(values = "Hello World.")
    private String moreText;

    @Override
    public String getText() {
        return text.getText();
    }

    @Override
    public boolean isRichText() {
        return text.isRichText();
    }
}

 

6 Replies

Thanks for the help but this is not a solution. I saw these answers you mention before posting this question. 

 

The first Q&A uses ...

FieldSetter.setField()

 ... but this is a deprecated method and field. 

 

The second Q&A literally mentions they cannot test overridden methods. 

 

The code you share in your repo also avoids unit testing an overridden method (or a ResourceSuperType). 

Avatar

Community Advisor

Hello @jeremylanssiers ,

You can follow this article where each step is described: https://medium.com/@sadyrifat/aem-unit-test-case-for-sling-delegation-pattern-1f011b947fb3 

Another GitHub commit link and repo for check your maven dependency also: https://github.com/Sady-Rifat/aem-demo/commit/81ce47fc28b70d31bee0034e2cadf69b721d892e 

Hope this will help you.

Avatar

Level 4

I have not been able to test your solution yet. However I was able to test a ResourceSuperType by directly injecting a mock value. E.g.

 

@Mock

CoreComponent coreComponent;

 

ExtendedCoreComponent extendedCoreComponent = new ExtendedCoreComponent();

 

extendedCoreComponent.coreComponent =coreComponent;

 

The obvious drawback is I had to make that coreComponent protected instead of private in my ExtendedCoreComponent class.

 

 

Avatar

Level 4

I was able to find a solution looking at the Unit test for WKND project's ImageList at 

https://github.com/adobe/aem-guides-wknd/blob/main/core/src/test/java/com/adobe/aem/guides/wknd/core...

The magic happens at these lines:

 

Field reader = ExtendedCoreComponentImpl.class.getDeclaredField("text");
reader.setAccessible(true);
reader.set(extendedCoreComponentImpl, text);


Class

 

package com.adobe.aem.guides.wknd.core.models.impl;

import com.adobe.aem.guides.wknd.core.models.ExtendedCoreComponent;
import com.adobe.cq.export.json.ComponentExporter;
import com.adobe.cq.export.json.ExporterConstants;
import com.adobe.cq.wcm.core.components.models.Text;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.models.annotations.Default;
import org.apache.sling.models.annotations.DefaultInjectionStrategy;
import org.apache.sling.models.annotations.Exporter;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.Via;
import org.apache.sling.models.annotations.injectorspecific.Self;
import org.apache.sling.models.annotations.injectorspecific.ValueMapValue;
import org.apache.sling.models.annotations.via.ResourceSuperType;

@Model(
        adaptables = SlingHttpServletRequest.class,
        adapters = { ExtendedCoreComponent.class,ComponentExporter.class},
        resourceType = ExtendedCoreComponentImpl.RESOURCE_TYPE,
        defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL
)
@Exporter(name = ExporterConstants.SLING_MODEL_EXPORTER_NAME, extensions = ExporterConstants.SLING_MODEL_EXTENSION)
public class ExtendedCoreComponentImpl implements ExtendedCoreComponent {

    static final String RESOURCE_TYPE = "wknd/components/extendedcorecomponent";

    @Self
    (type = ResourceSuperType.class)
    private Text text;

    @ValueMapValue
    (values="Hello World.")
    private String moreText;

    public String getMoreText() {
        return moreText;
    };

    
    public String getText() {
        return text.getText();
    }

    
    public boolean isRichText() {
        return text.isRichText();
    }

    
    public String getExportedType() {
        return ExtendedCoreComponentImpl.RESOURCE_TYPE;
    }
}

 

 

Mock data

 

{
  "jcr:primaryType": "nt:unstructured",
  "moreText": "moreText",
  "id": "id",
  "text": "<p>text</p>",
  "sling:resourceType": "wknd/components/extendedcorecomponent",
  "textIsRich": "true"
}

 

 

Unit Test

 

package com.adobe.aem.guides.wknd.core.models.impl;

import com.adobe.aem.guides.wknd.core.utils.TestUtil;
import com.adobe.cq.wcm.core.components.models.Text;
import io.wcm.testing.mock.aem.junit5.AemContext;
import io.wcm.testing.mock.aem.junit5.AemContextExtension;
import org.apache.sling.testing.mock.sling.ResourceResolverType;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.lang.reflect.Field;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.when;

@ExtendWith({AemContextExtension.class, MockitoExtension.class})
class ExtendedCoreComponentImplTest {
    private final AemContext aemContext = new AemContext(ResourceResolverType.JCR_MOCK);

    private static final String JCR_PATH = TestUtil.getMockJcrPath("extendedCoreComponent");
    private static final String MOCK_DATA = "/components/extendedCoreComponent/extendedCoreComponent.json";
    private static final String MOCK_DATA_EMPTY = "/components/extendedCoreComponent/extendedCoreComponent-empty.json";

    @Mock
    private Text text;

    @InjectMocks
    private ExtendedCoreComponentImpl extendedCoreComponentImpl;

    @BeforeEach
    void setup() {
        aemContext.addModelsForPackage(TestUtil.CORE_PACKAGES);
    }

    @Nested
    class ExtendedComponent {
        @BeforeEach
        void setUp() {
            aemContext.load().json("/pages/pages.json", "/content");
        }

        
        void emptyComponent() {
            extendedCoreComponentImpl = TestUtil.loadSlingModelViaSlingHttpServletRequest(aemContext, ExtendedCoreComponentImpl.class, MOCK_DATA_EMPTY, JCR_PATH);

            assertEquals("Hello World.", extendedCoreComponentImpl.getMoreText());
        }

        
        void component() {
            extendedCoreComponentImpl = TestUtil.loadSlingModelViaSlingHttpServletRequest(aemContext, ExtendedCoreComponentImpl.class, MOCK_DATA, JCR_PATH);

            assertEquals("moreText", extendedCoreComponentImpl.getMoreText());
        }
    }

    @Nested
    class CoreComponent {
        @BeforeEach
        void setup() {
            aemContext.load().json(MOCK_DATA, "/content");
            aemContext.addModelsForClasses(Text.class);
        }

        
        void coreComponent() throws NoSuchFieldException, IllegalAccessException {
            extendedCoreComponentImpl = TestUtil.loadSlingModelViaSlingHttpServletRequest(aemContext, ExtendedCoreComponentImpl.class, MOCK_DATA, JCR_PATH);

            Field reader = ExtendedCoreComponentImpl.class.getDeclaredField("text");
            reader.setAccessible(true);
            reader.set(extendedCoreComponentImpl, text);

            when(text.isRichText()).thenReturn(true);
            when(text.getText()).thenReturn("abc");

            assertTrue(extendedCoreComponentImpl.isRichText());
            assertEquals("abc", extendedCoreComponentImpl.getText());
        }
    }
}

 


Test Util Class

 

package com.adobe.aem.guides.wknd.core.utils;

import io.wcm.testing.mock.aem.junit5.AemContext;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.factory.ModelFactory;
import org.apache.sling.testing.mock.sling.servlet.MockSlingHttpServletRequest;

public class TestUtil {

    public static final String CORE_PACKAGES = "com.adobe.aem.guides.wknd.core";
    private static final String JCR_PAGE_PATH = "/content/project/country/language/page/jcr:content/root/container/container/%s";

    public static String getMockJcrPath(String componentName) {
        return String.format(JCR_PAGE_PATH, componentName);
    }

    public static <T> T loadSlingModel(AemContext aemContext, Class<T> clazz, String mockDataPath, String mockJcrPath) {

        boolean isAdaptableFromResource = false;
        boolean isAdaptableFromSlingHttpServletRequest = false;

        Class<?>[] adaptables = clazz.getAnnotation(Model.class).adaptables();

        for (Class<?> adaptable : adaptables) {
            if (StringUtils.equals(adaptable.getName(), Resource.class.getName())) {
                isAdaptableFromResource = true;
            }

            if (StringUtils.equals(adaptable.getName(), SlingHttpServletRequest.class.getName())) {
                isAdaptableFromSlingHttpServletRequest = true;
            }
        }

        if (isAdaptableFromResource) {
            return loadSlingModelViaResource(aemContext, clazz, mockDataPath, mockJcrPath);
        }

        if (isAdaptableFromSlingHttpServletRequest) {
            return loadSlingModelViaSlingHttpServletRequest(aemContext, clazz, mockDataPath, mockJcrPath);
        }

        return null;
    }

    public static <T> T loadSlingModelViaSlingHttpServletRequest(AemContext aemContext, Class<T> clazz, String mockDataPath, String mockJcrPath) {
        aemContext.load().json(mockDataPath, mockJcrPath);

        Resource resource = aemContext.resourceResolver().getResource(mockJcrPath);
        MockSlingHttpServletRequest request = aemContext.request();
        request.setResource(resource);
        ModelFactory modelFactory = aemContext.getService(ModelFactory.class);
        return modelFactory.createModel(request, clazz);
    }

    public static <T> T loadSlingModelViaResource(AemContext aemContext, Class<T> clazz, String mockDataPath, String mockJcrPath) {
        aemContext.load().json(mockDataPath, mockJcrPath);

        Resource resource = aemContext.resourceResolver().getResource(mockJcrPath);
        ModelFactory modelFactory = aemContext.getService(ModelFactory.class);
        return modelFactory.createModel(resource, clazz);
    }
}