JUnit Tests in AEM 6.5 with AEM Context

How to set up junit tests with AEM Context in AEM

Goal

Set up junit tests using AEMContext in a AEM project. We will create a generic Abstract Class that will be used to execute junit testing for different use cases (Servlets, Models, Schedulers, etc).

Procedure

First of all, have a look at your POM files. When you create a project using an AEM project archetype, you should already have different dependencies useful for Unit Testing. Since we are using AEM Mocks, please check if these ones are already there, otherwise add them:

 <!-- https://mvnrepository.com/artifact/org.mockito/mockito-core -->
            <dependency>
                <groupId>org.mockito</groupId>
                <artifactId>mockito-core</artifactId>
                <version>3.6.0</version>
                <scope>test</scope>
            </dependency>
            <dependency>
                <groupId>io.wcm</groupId>
                <artifactId>io.wcm.testing.aem-mock</artifactId>
                <version>2.2.8</version>
                <scope>test</scope>
            </dependency>

Now we can create our Java class. You will find different uses of Mocks. It’s up to you to check whether are used in your project, and if you prefer to include them just in specific classes instead of a generic one.

package com.adobe.training.core.models;

import com.adobe.acs.commons.models.injectors.annotation.impl.AemObjectAnnotationProcessorFactory;
import com.adobe.acs.commons.models.injectors.impl.AemObjectInjector;
import com.adobe.training.core.service.Lab2020Configuration;
import com.adobe.training.core.service.impl.Lab2020ConfigurationImpl;
import com.day.cq.replication.Replicator;
import com.day.cq.search.Query;
import com.day.cq.search.QueryBuilder;
import com.day.cq.search.result.SearchResult;
import io.wcm.testing.mock.aem.junit.AemContext;
import junit.framework.TestCase;
import junitx.util.PrivateAccessor;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.apache.sling.api.resource.ValueMap;
import org.apache.sling.settings.SlingSettingsService;
import org.apache.sling.settings.impl.SlingSettingsServiceImpl;
import org.apache.sling.testing.mock.sling.servlet.MockSlingHttpServletRequest;
import org.junit.Before;
import org.junit.Rule;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.osgi.framework.BundleContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Created by r.teruzzi on 04/01/2018.
 */
@RunWith(MockitoJUnitRunner.class)
public abstract class AbstractBaseTest extends TestCase {

    public static final String MODELS_PACKAGE = "com.adobe.training.core.models";
    public static final String LAB2020_CONFIGURATION_CONFIG_PATH = "/apps/Lab2020/config/com.adobe.training.core.service.general.impl.Lab2020ConfigurationImpl.config";

    public Logger LOG = LoggerFactory.getLogger(getClass());

    @Rule
    public AemContext aemContext = new AemContext();

    AemObjectAnnotationProcessorFactory factory = new AemObjectAnnotationProcessorFactory();

    //LIST OF COMMON MOCK OBJECTS YOU MAY USE
    @Mock
    private ResourceResolverFactory resourceResolverFactory;

    @Mock
    public SlingSettingsServiceImpl settingsService;

    @Mock
    protected Replicator replicator;

    //IF you have to execute queries
    @Mock
    public QueryBuilder queryBuilder;

    @Mock
    public Query query;

    @Mock
    public SearchResult searchResults;

    //LIST OF INJECT MOCKS
    @InjectMocks
    public AemObjectInjector aemObjectInjector;

    //E.G for CUSTOM OSGI SERVICE
    @InjectMocks
    public Lab2020ConfigurationImpl lab2020Configuration;


    //RETRIEVED WITH AEM CONTEXT

    public MockSlingHttpServletRequest request;

    protected ResourceResolver resourceResolver;

    protected BundleContext bundleContext;


    @Before
    @Override
    public void setUp() throws NoSuchFieldException {
        aemContext.addModelsForPackage(MODELS_PACKAGE);
        request = aemContext.request();
        resourceResolver = aemContext.resourceResolver();
        bundleContext = aemContext.bundleContext();

        loadCommonContent();
        registerOsgiConfigurations();
        registerServices();
    }

    protected abstract void loadCommonContent();
    //aemContext.load().json("/crxContent/content.json", CONTENT); EXAMPLE - HOW YOU WILL LOAD CONTENT


    //Example - How to set OSGI properties
    protected void registerOsgiConfigurations() throws NoSuchFieldException {
        registerResourceForServices();
        Resource configsResource = resourceResolver.getResource(LAB2020_CONFIGURATION_CONFIG_PATH);
        ValueMap valueMap = configsResource.getValueMap();
        String endpoint1 = valueMap.get("getEndpoint1", String.class);
        String endpoint2 = valueMap.get("getEndpoint2", String.class);

        PrivateAccessor.setField(this.lab2020Configuration, "endpoint1", endpoint1);
        PrivateAccessor.setField(this.lab2020Configuration, "endpoint2", endpoint2);
    }


    private void registerServices() {
        aemContext.registerInjectActivateService(factory);
        aemContext.registerService(AemObjectInjector.class, aemObjectInjector);
        aemContext.registerService(ResourceResolverFactory.class, resourceResolverFactory);
        aemContext.registerService(Lab2020Configuration.class, this.lab2020Configuration);
        aemContext.registerService(QueryBuilder.class, queryBuilder);
        aemContext.registerService(SlingSettingsService.class, settingsService);
    }

    private void registerResourceForServices() {
        aemContext.load().json("/osgiConfig/lab2020configuration.json", LAB2020_CONFIGURATION_CONFIG_PATH);
    }

}

Since unit tests are executed at build, outside the context of a running AEM instance, there is no repository and resources. To facilitate this, wcm.io’s AEM Mocks creates a mock context that allows these APIs to mostly act as if they are running in AEM.

AEM Mocks is a mock implementation of the most important parts of AEM API (Resource Resolver, Resource, Page/PageManager, DAM and so on) and it highly depends on OSGi Mocks, Sling Mocks, JCR Mocks.
A JUnit Rule object “AemContext” gives easy access to all AEM/Sling context objects and allows you to perform operations such as:

  • Load content from JSON Files (create your CRX repository)
  • Register OSGi services and Sling Models
  • Manipulate current request/response states

If you want to know more about AEM Mocks, check this link: https://wcm.io/testing/aem-mock/usage.html

There are different initializations of the AEM Context:

  • RESOURCERESOLVER_MOCK (default)
    • Mocked Resource Resolver from Sling Testing
    • Fastest, but does not support JCR
    • Eventing support, but no Search
  • JCR_MOCK
    • Mocked JCR with real Sling Resource-JCR Mapping
    • Still very fast
    • Not all JCR features support (e.g. no Search, Observation)
  • JCR_OAK
    • Real OAK with real Sling Resource-JCR (“CRX3”)
    • Full support for Node Types, Search, Observation etc.
  • CR_JACKRABBIT
    • Real Jackrabbit with real Sling Resource-JCR (“CRX2”)
    • Problem: Is not cleaned up for each test run, Tests have to use unique paths and clean up themselves

What you could do, if you need it, is to initialize again the context in your subclass with a more specific resource resolver type!

If would like to know more: https://sling.apache.org/documentation/development/sling-mock.html#resource-resolver-types

Let’s go on through the code. After the AemContext you can see different @Mock and @InjectMocks. Remember that @Mock creates a mock. @InjectMocks creates an instance of the class and injects the mocks that are created with the @Mock.
Behaviors of Mock objects must be modeled by using when and thenReturn method.

Then, in the setUp method, we have to:

  • addModelsForClasses: registers the Sling Model to be tested, into the mock AEM Context, so it can be instantiated in the @Test methods.
  • load().json: loads resource structures into the mock context, allowing the code to interact with these resources as if they were provided by a real repository. In each class you will override the loadCommonContent method loading the content JSON that more suits for your tests.
  • registerServices: registers the OSGi services. There is also an example about how to initialize a service with configured properties (registerOsgiConfigurations)

This is how your test folder will look like. The java folder with your test classes, and the resources with the JSON files for your context, and some JSON files used as expected results in the assertEqual conditions, for instance for your servlets tests!

This is just the file used in the example above for the OSGi configuration:

{
  "jcr:primaryType": "sling:OsgiConfig",
  "getEndpoint1":"/v1/users",
  "getEndpoint2":"/v2/users"
}

If you’re wondering which is the related OSGi service, it is taken from here. It’s just used as example.

Now you can start testing all your models, servlets, etc! Here I’m loading all the articles related to Unit Testing.

Cheers! 🍻

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: