Expand my Community achievements bar.

Don’t miss the AEM Skill Exchange in SF on Nov 14—hear from industry leaders, learn best practices, and enhance your AEM strategy with practical tips.

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);
    }
}