aem-Simplify unit testing with aemcontextcallback

AEM: Simplify unit testing with AemContextCallback

For testing (you do this, right?) your AEM project, most probably, you use AEM Mocks from wcm.io. This is a powerful library, which makes your life easier when it comes to testing Sling Models, Servlets, other classes which work with Page and other entities from AEM.

How to use AemContextCallback

Interface AemContextCallback allows us to define some default operations which are common for all our test classes, e.g. to register our models.

import io.wcm.testing.mock.aem.junit.AemContext;
import io.wcm.testing.mock.aem.junit.AemContextCallback;

public class AppContextSetup implements AemContextCallback {

    @Override
    public void execute(final AemContext aemContext) throws Exception {
        aemContext.addModelsForPackage("com.taradevko.aem");
    }
}

Now, when we define JUnit rule for AemContext, we should create and pass our callback into AemContext‘s constructor.

public final AppContextSetup appContextSetup = new AppContextSetup();
    
@Rule
public AemContext aemContext = new AemContext(appContextSetup, ResourceResolverType.RESOURCERESOLVER_MOCK);

After these changes, all our models in the defined package will be initialized. There is no need to do this explicitly anymore.

Mixing AemContextCallback and TestRule

So far, our callback does not make our life much easier, but we definitely can take more out of AemContextCallback! When you need to create some resource for a class under the test, you either build it directly in your test class (or some helper) or you load it from the file (e.g. json). We can easily move this logic into or callback implementing additionally JUnit’s TestRule interface. Let’s take a look at this:

import io.wcm.testing.mock.aem.junit.AemContext;
import io.wcm.testing.mock.aem.junit.AemContextCallback;
import org.apache.sling.api.resource.Resource;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;

public class AppContextSetup implements AemContextCallback, TestRule {

    private Class testClass;

    @Override
    public void execute(final AemContext aemContext) throws Exception {
        aemContext.addModelsForPackage("com.taradevko.aem");

        final Resource content = aemContext.load().json(
                String.format("/%s/%s.json", testClass.getPackage().getName(), testClass.getSimpleName()), "/content");
        aemContext.currentResource(content);
    }

    @Override
    public Statement apply(final Statement base, final Description description) {
        testClass = description.getTestClass();
        return base;
    }
}

So what is going on here? Method apply is called for each test in our test class. Here we just store this class to reuse later.

Then, when AemContext executes our callback, we build a path “/class_package/class_name.json”, load it with ContentLoader and set as a current resource.

Time for testing

Now it’s time to use it. Imagine, we have a simple model we want to test:

import org.apache.sling.api.resource.Resource;
import org.apache.sling.models.annotations.Model;

import javax.inject.Inject;

@Model(adaptables = Resource.class)
public class SomeModel {

    @Inject
    private String property1;

    public String doSmthWithProperty() {
        return property1.toUpperCase();
    }
}

And also we prepared a json file with a resource to adapt to this model for test purposes. It’s located under “com.taradevko.aem.contcallback/SomeModelTest.json” in test resources section.

{
  "jcr:primaryType": "nt:unstructured",
  "property1": "jsonProp1Value"
}

And now, finally – testing:

package com.taradevko.aem.contcallback;

import com.taradevko.aem.AppContextSetup;
import io.wcm.testing.mock.aem.junit.AemContext;
import org.apache.sling.testing.mock.sling.ResourceResolverType;
import org.junit.Rule;
import org.junit.Test;

import static org.junit.Assert.*;

public class SomeModelTest {

    @Rule
    public final AppContextSetup appContextSetup = new AppContextSetup();

    @Rule
    public AemContext aemContext = new AemContext(appContextSetup, ResourceResolverType.RESOURCERESOLVER_MOCK);

    @Test
    public void demoTest() {
        final SomeModel someModel = aemContext.currentResource().adaptTo(SomeModel.class);
        assertEquals("Should contain value from json", someModel.doSmthWithProperty(), "JSONPROP1VALUE");
    }
}

As before, we create AppContextSetup and pass it to the constructor of AemContext. But now it also got annotation @Rule.

Now, if we run the example, we can see that test passed successfully!

AemContextCallback test run example

Now, whenever we need to load resources from json, we just should put it on the correct path, and use our AppContextSetup. The rest will be done automatically!

What is next

You can do much more with AemContextCallback, like registering you stub implementations of OSGi services or executing additional callbacks on AemContext.

You can find the complete example in this repository.

Your thoughts are welcome