Unit Testing AEM Sling Models and Servlets

Published

As teams and code-bases grow, dependencies criss-crossing the code-base become more difficult to track. Unit tests help ensure code modules work consistently and predictably so that any referencing code doesn’t have to.

A growing list of OSGi @References and dependencies..

Code Referred to in this Post

Throughout this post I will be showing snippets of code from the AEM Project Archetype. A few of the other examples are taken from open source projects like ACS AEM Commons and the AEM WCM Core Components. I created generated the archetype code with:

mvn -B archetype:generate \
 -D archetypeGroupId=com.adobe.aem \
 -D archetypeArtifactId=aem-project-archetype \
 -D archetypeVersion=24 \
 -D appTitle="AEM Unit Test Demo" \
 -D appId="unitdemotest" \
 -D groupId="me.callsen.taylor" \
 -D aemVersion="6.5.0"

Basic Anatomy of a Sling Model Unit Test

The sample JUnit tests provided as part of the AEM Project Archetype are a great place to start. The HelloWorldModelTest inside of ./core/src/test/java/me.callsen.taylor.core/models reveals three important components of unit testing.

1. @BeforeEach Functions and Test Setup

Screenshot of code from the AEM Project Archetype, version 24

The @BeforeEach function is executed before each unit test is run, and is responsible for setting up the “Testing Context”. This includes loading any test content, creating mock pages/resources, and registering Services and Adapters.

2. Testing Context and the AemContext Object

The Testing Context is where all resources (content, OSGi Services, Adaptables, etc.) are collected and registered for easy use in the unit tests. Think about it like a registry that stores references to each of the external resources; without it, the test wouldn’t know where to find it’s dependencies.

Examples of registering these dependencies can be found below.

3. @Test Execution Functions

Screenshot of code from the AEM Project Archetype, version 24

Functions annotated with an @Test contain the actual testing logic. A test should focus on a specific function of the model, and ensure the outcome is the same each time. Similar to other JUnit tests, assertTrue() and assertEquals() functions are used to verify function output. Generally a @Test function is created for each getter function, or any function of a model that executes “controller” type logic.

Mocking AEM Resources

Below are a few techniques to make the various resources used through-out the AEM ecosystem available in unit tests. In that majority of cases, this involves registering content or Classes with the AEM Context Object.

Loading Sample Content

We’ll need come sample content to execute our unit tests against. We can create sample nodes programmatically:

// create a resource and set as currentResource
context.build().resource("/content/myResource", "jcr:title", "My Title").commit();
context.currentResource("/content/myResource");

// create a page and set as currentPage
context.create().page("/content/mysite/en", "/apps/myapp/template/homepage");
context.currentPage("/content/mysite/en");

We can also import content directly from a JSON file, which is helpful when working with larger datasets:

// load json data into a specific location
context.load().json("/my-sample-data.json", "/content/mysite/en");

For more information, see the wcm.io’s content loading documentation here.

Registering an OSGi Service

If the model being tested contains @References to custom OSGi Services, we’ll need to register these Services with the AEMContext Object. Fortunately the registerService() function makes this easy to do:

context.registerService(ImplementationPicker.class, new ResourceTypeBasedResourcePicker());

Simply specify the Class of the OSGi @Reference and supply the Object to be returned. The Testing Context (AemContext Object) will now know which Object to return whenever the @Reference class is used in a model or unit test.

For more information and examples, check out the official OSGi Mock documentation.

Registering a Sling Adapter

The adaptTo() interface is one of the more fun-to-use features of AEM and Sling. While common Adaptables are defined in the Testing Context out of the box, custom ones need to be explicitly registered. Fortunately the AemContext object provides the registerAdapter() function for this purpose:

Session mockSession = mock(Session.class);
context.registerAdapter(ResourceResolver.class, Session.class, mockSession);

In this example, we are using the Mockito Testing Framework to create a mock (skeleton) Session class, and telling the AemContext Object to return this mocked class anytime a ResourceResolved is adapted to a Session (e.g. resourceResolver.adaptTo(Session.class)).

The AEM WCM Core Components repo has several examples of creating mock classes and registering them as Adaptables.

Mocking Sling Service Functions

Mocking Sling Service function calls can help extend tests to include situations where external service are used, or situations that are too difficult to recreate in a test.

Consider the example below, where the test is dependent on a third-party API. Using Mockit’s collection of functions (e.g. when() and thenReturn()), I can easily simulate the API response:

@Mock
private PhotoApiService photoApiService;

@Test
void testPhotoApi(AemContext context) {        
  
  when(photoApiService.getPhotoList().thenReturn(new ArrayList()));

  List photoList = photoApiService.getPhotoList();

  // rest of testing logic

}

The Mockito functions will also apply if the getPhotoList() call is made inside an included Model or Service, not just inside the test logic.

Testing a Sling Servlet

The AEM Project Archetype has a great example for testing Sling Servlets. Simple open the provided SimpleServletTest inside of ./core/src/test/java/me.callsen.taylor.core/servlets. Here it is, taken directly from archetype version 24:

@ExtendWith(AemContextExtension.class)
class SimpleServletTest {

  private SimpleServlet fixture = new SimpleServlet();

  @Test
  void doGet(AemContext context) throws ServletException, IOException {
    context.build().resource("/content/test", "jcr:title", "resource title").commit();
    context.currentResource("/content/test");

    MockSlingHttpServletRequest request = context.request();
    MockSlingHttpServletResponse response = context.response();

    fixture.doGet(request, response);

    assertEquals("Title = resource title", response.getOutputAsString());
  }
}

The Servlet is loaded as a fixture; after creating a sample resource, a GET request is placed against that resource passing through the the Servlet being tested.

Other Resources

Here are a few resources that can be helpful and provide examples when writing AEM Unit Tests:

Subscribe by Email

Enter your email address below to be notified about updates and new posts.


Comments

Loading comments..

No responses yet

Leave a Reply

Your email address will not be published. Required fields are marked *