---
title: Unit Testing Using AEM Mocks
description: Learn how unit testing with AEM Mocks ensures flawless functionality in AEMaaCS, enhances user experience, and maintains high platform performance.
publish date: 2024-12-09
author: Jonathan Saurez
image: https://www-qa.oshyn.com/-/media/Oshyn/Insights/Blog/2024-12-10-Unit-Testing-Using-AEM-Mocks/blog_hero_aem-mocks-unit-testing.jpg?rev=a371dae06af140c6a5d3d3ce4e72c8c0
url: http://www-qa.oshyn.com/blog/2024/12/aem-mocks-unit-testing
---
# Unit Testing Using AEM Mocks

![Young Developer performing Unit Testing](https://www-qa.oshyn.com/-/media/Oshyn/Insights/Blog/2024-12-10-Unit-Testing-Using-AEM-Mocks/blog_hero_aem-mocks-unit-testing.jpg?rev=a371dae06af140c6a5d3d3ce4e72c8c0&hash=FA46DA7EF2111AE52AB6533C13CED73D)

Faulty code isn’t just a pain for the development team; it can also negatively impact the user experience. Businesses using Adobe Experience Manager as a Cloud Service (AEMaaCS) must ensure that their website features function properly to deliver the best digital experience to their customers. That functionality can be ensured with unit testing and AEM Mocks.

By simulating real-world scenarios and identifying code flaws early, AEM Mocks allows your development teams to catch issues that could otherwise disrupt user experiences, ensuring that your platform remains reliable and high-performing.

In this post, we’ll explore the basics of unit testing and walk through how to perform unit tests in AEM.

## Prerequisites

This post will be particularly useful for AEM developers who have the following characteristics:

- Understands/has experience with basic AEM concepts like:
  - HTL
  - AEM Dialogs
  - Sling Models
  - OSGi services
- Has an Adobe AEM instance already up and running.
- Knows how to build and deploy an AEM project to an AEM instance.
- Has a project based on the AEM Project Archetype that builds and deploys correctly to an AEM instance.
- Basic knowledge about Unit Testing and Assertions.

For this example, we will work inside the WKND project, commonly available to any AEM instance.

## Unit Testing Basics

### What Is Unit Testing?

As the name suggests, unit testing is the process of testing units, where units are the smallest functional and testable pieces of code in a project.

Unit tests automatically detect code flaws in the early stages of development, giving more time to fix bugs and, if implemented correctly, improving code quality. (If a project is configured to error out and not compile if unit tests do not pass, it makes it harder for developers to introduce bugs).

### Unit Testing in AEM

In the AEMaaCS world, Unit Testing development became a requisite as the Adobe Cloud Manager CI/CD pipeline integrates Unit Test execution and coverage reports.

The AEM Maven Archetype automatically includes 3 Testing Frameworks in its parent POM:

- JUnit5
- Mockito
- AEM Mocks.

### Where are Unit Tests Located?

Unit tests are located under:

src/test/java

The classes to be tested are located under:

src/main/java

### Unit Tests Naming

Unit Test classes follow almost the same fully qualified name (package and class name) as the class to be tested, but per convention, add a “Test” suffix to the class name.

#### Example

So, if the fully qualified name of the class to be tested is:

src/main/java → com.pckg1.pckg2.pckg3.myClass

Then its unit test fully qualified name needs to be:

src/test/java → com.pckg1.pckg2.pckg3.myClassTest

## Unit Testing in AEM (io.wcm.testing.mock.aem)

Characteristics of io.wcm.testing.mock.aem library:

- It provides classes that enable an in-memory AEM environment, which allows developers to Unit Test an AEM project functionality (around 90% of it at least).
- It is compatible with Mockito
- It is compatible with AEM 6.4, 6.5 and AEMaaCS

Notes:

- Transitive dependencies should not be excluded
- Use transitive dependencies according to the AEM being used (look at AEM Dependencies | wcm.io)
- Some plugins are available; check the List of AEM Mock Context Plugins

### 1. Maven Dependency


```
<dependency>

   <groupId>io.wcm</groupId>

   <artifactId>io.wcm.testing.aem-mock.junit5</artifactId>

   <version>4.1.9</version>

   <scope>test</scope>

</dependency>
```

Note: AEM Mocks dependencies should be defined in the POM before AEM API dependency.

### 2. junit5.AemContext

The AEMContext class acts like a virtual AEM sandbox environment. It gets instantiated as a regular object but requires a resource type to be specified:

AemContext aemContext = new AemContext(ResourceResolverType.ONE_OF_THE_AVAILABLE_TYPES);

The current available

ResourceResolverType (AEM Mocks JUnit 5 4.1.9-SNAPSHOT API) are:

| ENUM Constant | Description | Provides |
| --- | --- | --- |
| RESOURCERESOLVER_MOCK | Uses Sling "resourceresolver-mock" implementation, no underlying JCR repository. It is the default type when no value is specified. | Sling API |
| JCR_MOCK | Uses a simple JCR "in-memory" mock as the underlying repository. It’s slower than RESOURCERESOLVER_MOCK. | Sling API & JCR API |
| JCR_OAK | Uses a real JCR Jackrabbit Oak repository. It’s the slowest. | Sling API & JCR API & Observation & JCR Queries |
| NONE | Provides resource resolver environment without any ResourceProvider. It’s the fastest option due to being lightweight. Best suited for tests that do not require a resource resolver, like tests of pure Java logic, and that require a lot of test cases. | Nothing |

#### The AEMContext class allows complex operations like:

##### Load JSON Content Structures and Map Them to Resource Paths

aemContext.load().json("/path/to/content-to-test.json", "/content/testingName");

Note: the JSON content file must be placed inside project/src/test/resources.

##### Create Mock Resources

aemContext.create().resource("resourcePath", propertiesMap) → check other implementation of the method

##### Create Mock Pages

Page rootPage = aemContext.create().page("/content/rootSitePage"); aemContext.create().page("/content/rootSitePage/childPage");

##### Create Mock Assets

Asset asset = context.create().asset("/content/dam/myAsset.jpg", 60, 90, "image/jpeg");

##### Set a Mock Resource as the Active One Been Tested

aemContext.currentResource("/path/to/my/resource");

##### Register Mock Services

aemContext.registerService(MyService.class, new MyServiceImpl());

##### Instantiate Sling Models and Services using adapTo()

aemContext.request().adaptTo(MyAdaptableSlingModel.class) aemContext.resourceResolver().adaptTo(MyService.class);

##### Mock Sling Requests and Responses

SlingHttpServletRequest request = aemContext.request(); context.request().setAttribute("attributeName", "attributeValue"); SlingHttpServletResponse response = aemContext.response();

##### Get Mocked Common Used Services

ResourceResolver resolver = aemContext.resourceResolver(); PageManager pageManager = aemContext.pageManager();

##### Set a Mock WCM Mode

aemContext.request().setAttribute(WCMMode.class.getName(), WCMMode.EDIT);

### 3.junit5.AemContextExtension

The AemContextExtension class is a JUnit 5 extension that allows injecting AemContext (or subclasses of it) parameters in test methods. It ensures that the context is set up and torn down properly for each test method, providing test utility methods like afterAll, afterEach, afterTestExecution, and beforeAll.

## Basic Common Development Use Case - Dialog Field or Parameter Value Used For Business Logic

Let’s work with a requirement that involves common AEM concepts:

### Requirement

“A company that sells Tour Packages wants to show a list of the tour packages available on their site. When a user clicks on a package of the list, the tours in that package are shown, and the URL of that specific search needs to be able to be bookmarked or shared with users the results they request. The component must be able to pre-filter by an initial category selected during the authoring phase.”

### The Solution in AEM

“The AEM developer is requested to create a component that reads a list of available tours from a service. Those tours are classified into different categories; every tour can belong to more than one category. Every tour package is offered as a group of tours in the same category. The component should show the currently selected category by the user, and the authors should be able to edit the component to select the initial tour category/package shown on the page.”

#### These are a couple of examples of how Tour Packages will be shown:

Water Tour Package

![Water Tour Package](https://www-qa.oshyn.com/-/media/Oshyn/Insights/Blog/2024-12-10-Unit-Testing-Using-AEM-Mocks/tour-package-water_aem-mocks-unit-testing.png?rev=3e0057e98dee47d4840bcfb74224eb02)

Mountain Tour Package

![Mountain Tour Package](https://www-qa.oshyn.com/-/media/Oshyn/Insights/Blog/2024-12-10-Unit-Testing-Using-AEM-Mocks/tour-package-mountain_aem-mocks-unit-testing.png?rev=b6c5086a659543cb9f13e7369c1ca7fb)

#### To solve this requirement, the developer will need to create:

1. An OSGi service that provides the tour data.
2. A Sling model that reads data from the OSGi Service and provides filtered data based on dialog configuration or values selected by the user.
3. An AEM Component that shows the tour data.
4. A dialog to configure the component's initial behavior.

![Tour Packages](https://www-qa.oshyn.com/-/media/Oshyn/Insights/Blog/2024-12-10-Unit-Testing-Using-AEM-Mocks/tourpackages_aem-mocks-unit-testing.webp?rev=533a0653245443f0ad283108d31ee77e)

#### Step By Step

1. An OSGi service that provides the tour data. In real life, data will come from a data repository, such as an RDBMS (Relational Database), Mongo DB, JSON file, REST API, etc. In this case, we are burning some data in code for example purposes. @Slf4j @Component(immediate = true, service = TourService.class) public class TourServiceImpl implements TourService { public TourServiceImpl() { } @Override public List<Tour> getTours(){ List<Tour> tours = new ArrayList<>(); List<String> categories = Arrays.asList(CATEGORY_WATER,CATEGORY_NATIONAL_PARK,CATEGORY_ADVENTURE); Tour currentTour = new TourImpl( categories, "Arenal Volcano lake paddle board and hot spring water", "We will paddleboard in the lake next to the volcano during the morning and finish the day in the hot spring waters", 350.0, "https://www.sinac.go.cr/ES/ac/BannersParques/Volcan-Arenal-eng.jpg" ); tours.add(currentTour); categories = Arrays.asList(CATEGORY_WATER,CATEGORY_NATIONAL_PARK,CATEGORY_ADVENTURE); currentTour = new TourImpl( categories, "Surfing Day at Witches Rock", "We take an 1.5h boat ride to spend the day surfing at the mythical Witches Rock!", 250.0, "https://www.sinac.go.cr/ES/ac/BannersParques/BannersIngles/santa-rosa-Ing.jpg" ); tours.add(currentTour); categories = Arrays.asList(CATEGORY_MOUNTAIN,CATEGORY_NATIONAL_PARK,CATEGORY_ADVENTURE); currentTour = new TourImpl( categories, "Hike Chirripo National Park", "We will hike around 20/25km per day and will sleep two nights in the mountains refugee, so we can get to the top of highest mountain of the country", 600.0, "https://www.sinac.go.cr/ES/ac/BannersParques/BannersIngles/chirripo-Ing.jpg" ); tours.add(currentTour); return tours; } }
2. A Sling model that reads data from the OSGi Service and provides filtered data based on dialog configuration or values selected by the user. As you can see, there are two ways to provide data for filtering: @Model( adaptables = {SlingHttpServletRequest.class, Resource.class }, adapters = TourPackage.class, defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL, resourceType = TourPackageImpl.RESOURCE_TYPE ) public class TourPackageImpl implements TourPackage { protected static final String RESOURCE_TYPE = "ROOT/COMPONENT/PATH/availabletours/v1/availabletours"; @Inject String paramCategory; @ValueMapValue @Default(values="") String dialogCategory; @OSGiService TourService serviceTours; @PostConstruct private void init() { } @Override public List<Tour> getAllTours(){ return serviceTours.getTours(); } @Override public String getParamCategory() { return paramCategory; } @Override public String getDialogCategory() { return dialogCategory; } @Override public List<Tour> getToursByCategory(){ String selectedCategory = EMPTY; if(isNotEmpty(paramCategory)) { selectedCategory = paramCategory; } else { if (isNotEmpty(dialogCategory)) { selectedCategory = dialogCategory; } } return getToursByCategory(selectedCategory); } @Override public List<Tour> getToursByCategory(final String selectedCategory) { if(isNotEmpty(selectedCategory)) { return serviceTours.getTours().stream().filter( tour -> tour.getCategories().stream().anyMatch( category -> category.equalsIgnoreCase(selectedCategory) ) ).collect(Collectors.toList()); } else { return new ArrayList<>(); } } @Override public List<String> getCategories(){ HashSet<String> categories = new HashSet<>(); for (Tour Tour: serviceTours.getTours() ) { categories.addAll(Tour.getCategories()); } List<String> categoriesList = new ArrayList<>(categories); Collections.sort( categoriesList ); return categoriesList; } }
  - a dialogCategory that can be set directly in the component dialog, so a specific category can shown as initial data
  - a paramCategory that can be sent from HTL. When a user selects a Category/Package, the page can be reloaded, and the sling model will use that value for filtering.
3. An AEM Component that shows the tours data <div data-sly-use.tourList="${'com.oshyn.blogs.unittesting.models.TourPackage'}"> <ul class="nav"> Tour Packages: <div data-sly-list.category="${tourList.categories}"> <li><a href="?category=${category}">${category}</a></li> </div> </ul> <div data-sly-test.toursByCategory="${tourList.toursByCategory}"> <div data-sly-list.tour="${toursByCategory}"> <ol> <hr> <h3>${tour.tourName}</h3> <img src="${tour.imageReference}" alt="${tour.tourName}" width="300" height="100" /> <li>Description: "${tour.description}"</li> <li>Price: $"${tour.price}"</li> <li>Type of Tour: <div data-sly-list.category="${tour.categories}"> <a href="?category=${category}">${category}</a> </div> </li> </ol> </div> </div> </div>
4. A dialog to configure the component's initial behavior (category values are burned in the dialog as per simplicity, but they should be dynamic, but that’s outside of the scope of this blog).
5. Note: the AEM component is created at ROOT/COMPONENT/PATH/tourpackages/v1/tourpackages <?xml version="1.0" encoding="UTF-8"?> <jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:granite="http://www.adobe.com/jcr/granite/1.0" xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0" jcr:primaryType="nt:unstructured" jcr:title="Tour Packages" sling:resourceType="cq/gui/components/authoring/dialog"> <content jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/coral/foundation/container"> <items jcr:primaryType="nt:unstructured"> <tabs ...> <items jcr:primaryType="nt:unstructured"> <tourCategories jcr:primaryType="nt:unstructured" jcr:title="Select the initially selected Tour Package" sling:resourceType="granite/ui/components/coral/foundation/container" margin="{Boolean}true"> <items jcr:primaryType="nt:unstructured"> <columns ...> <items jcr:primaryType="nt:unstructured"> <column ...> <items jcr:primaryType="nt:unstructured"> <categories ...> <items jcr:primaryType="nt:unstructured"> <dialogCategory jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/coral/foundation/form/select" fieldLabel="Category" name="./dialogCategory" granite:class="foundation-toggleable"> <items jcr:primaryType="nt:unstructured"> <def jcr:primaryType="nt:unstructured" text="(default)" value=""/> <water jcr:primaryType="nt:unstructured" text="Water" value="Water"/> <nationalpark jcr:primaryType="nt:unstructured" text="National Park" value="National Park"/> <adventure jcr:primaryType="nt:unstructured" text="Adventure" value="Adventure"/> <mountain jcr:primaryType="nt:unstructured" text="Mountain" value="Mountain"/> </items> </dialogCategory> </items> </categories> </items> </column> </items> </columns> </items> </tourCategories> <cq:styles jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/coral/foundation/include" path="/mnt/overlay/cq/gui/components/authoring/dialog/style/tab_edit/styletab"/> </items> </tabs> </items> </content> </jcr:root>

## Unit Tests for Use Case

The goal is always to automatically verify that 100% of the code works as expected, but testing every single line of code can be challenging. Lines for edge cases might require a lot of time mocking data to simulate the scenario, so at least 80% is acceptable.

### Let's Start Testing a Sling Model (TourPackage)

This Sling Model should be able to grab an initial filtering value from the AEM authoring dialog, so some specific AEM capabilities to mock that behavior are required.

### 1. Creating Mocked Page Content

core/src/test/resources/com/oshyn/blogs/unittesting/models/ToursTest.json


```
{
  "tours-page-dialog-value-water": {
    "jcr:primaryType": "nt:unstructured",
    "sling:resourceType": "ROOT/COMPONENT/PATH/tourpackages/v1/tourpackages",
    "dialogCategory": "Water"
  },
  "tours-page-empty-dialog": {
    "jcr:primaryType": "nt:unstructured",
    "sling:resourceType": "ROOT/COMPONENT/PATH/tourpackages/v1/tourpackages"
  }
}
```

### 2. Setting the Testing Environment for All Tests

To set up an AEM mock environment, some basic steps need to be done:

1. Instantiate the current AEM Mocking Context with one of the ResourceResolverTypes discussed before, according to the requirements of the test
2. Use the @BeforeEach to set a common group of operations required for all consequent tests, like: package com.oshyn.blogs.unittesting.models; import com.oshyn.blogs.unittesting.models.v1.TourImpl; import com.oshyn.blogs.unittesting.models.v1.TourPackageImpl; import com.oshyn.blogs.unittesting.services.impl.v1.TourServiceImpl; 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.Test; import org.junit.jupiter.api.extension.ExtendWith; import static org.junit.jupiter.api.Assertions.*; import static com.oshyn.blogs.unittesting.models.TourCategories.*; //other imports @ExtendWith({AemContextExtension.class}) public class TourPackageTest { //instantiates the current AEM Mocking Context private final AemContext aemContext = new AemContext(ResourceResolverType.RESOURCERESOLVER_MOCK); public static final String PARAMETER_CATEGORY = "paramCategory"; TourPackage tourPackage; @BeforeEach public void setUp(){ //load the JSON file that mocks the page content and assigning it to a "testing" content path aemContext.load().json( "/com/oshyn/blogs/unittesting/models/ToursTest.json", "/content" ); //register and inject a tour service in the current context aemContext.registerInjectActivateService(new TourServiceImpl()); } //test methods will come here }
  - Loading mocked resources
  - Register and inject in the context services required by the class to be tested

### 3. Testing Methods That Only Require the Sling Model and its Dependencies

The TourPackage Sling Model can be adapted from/to a SlingHttpServletRequest, and the context can provide such an object, so it’s a matter of adapting it to use it.


```
@Test void tes_getCategories(){
    TourPackage tourPackage = aemContext.request().adaptTo(TourPackageImpl.class);
    assertNotNull(tourPackage);
    assertFalse(tourPackage.getCategories().isEmpty());
}
@Test void test_getAllTours(){
    TourPackage tourPackage = aemContext.request().adaptTo(TourPackageImpl.class);
    assertNotNull(tourPackage);
    assertFalse(tourPackage.getAllTours().isEmpty());
}
```

### 4. Testing Getting a Dialog Field Value

As the category dialog value needs to be tested, it must load the specific content set from the JSON-mocked data.


```
@Test void test_getDialogCategory(){
    //loading the specific content set from the JSON mocked data
    aemContext.currentResource("/content/tours-page-dialog-value-water");
    tourPackage = aemContext.request().adaptTo(TourPackageImpl.class);
    assertNotNull(tourPackage);
    assertEquals(CATEGORY_WATER, tourPackage.getDialogCategory());
}
```

### 5. Testing Getting a Request Parameter

The category parameter value needs to be tested, so the parameter and its value must be set in the current AEM context request.


```
@Test void test_getParamCategory(){
    aemContext.request().setAttribute(PARAMETER_CATEGORY, CATEGORY_MOUNTAIN);
    tourPackage = aemContext.request().adaptTo(TourPackageImpl.class);
    assertNotNull(tourPackage);
    assertEquals(CATEGORY_MOUNTAIN, tourPackage.getParamCategory());
}
```

### 6. Testing Logic Based on a Parameter Value

As the logic of filtering data based on a parameter value needs to be tested, the specific content set from the JSON mocked data must be loaded, and the parameter and its value must be set in the current AEM context request. After that, the expected result is compared with the values returned by the filtering method.


```
@Test void test_getToursByCategory_from_parameter(){
    aemContext.currentResource("/content/tours-page-dialog-value-water");
    aemContext.request().setAttribute(PARAMETER_CATEGORY, CATEGORY_MOUNTAIN);
    tourPackage = aemContext.request().adaptTo(TourPackageImpl.class);
    assertNotNull(tourPackage);
    //loading expected data
    List<Tour> expectedTours = new ArrayList<>();
    List<String> categories = Arrays.asList(CATEGORY_MOUNTAIN,CATEGORY_NATIONAL_PARK,CATEGORY_ADVENTURE);
    Tour currentTour = new TourImpl(
            categories,
            "Hike Chirripó National Park",
            "We will hike around 20/25km per day and will sleep two nights in the mountains refugee, so we can get to the top of highest mountain of the country",
            600.0,
            "https://www.sinac.go.cr/ES/ac/BannersParques/BannersIngles/chirripo-Ing.jpg"
    );
    expectedTours.add(currentTour);
    //testing against expected values
    assertFalse(tourPackage.getToursByCategory().isEmpty());
    assertEquals(tourPackage.getToursByCategory().size(), expectedTours.size());
    assertEquals(tourPackage.getToursByCategory().get(0).getCategories(), expectedTours.get(0).getCategories());
    assertEquals(tourPackage.getToursByCategory().get(0).getTourName(), expectedTours.get(0).getTourName());
}
```

### 7. Testing Getting an Invalid Parameter Value

Because an invalid category parameter value must be tested, the parameter and its value must be set in the current AEM context request.


```
@Test
void test_getToursByCategory_from_invalid_parameter(){
    aemContext.request().setAttribute(PARAMETER_CATEGORY, EMPTY);
    aemContext.currentResource("/content/tours-page-empty-dialog");
    tourPackage = aemContext.request().adaptTo(TourPackageImpl.class);
    assertNotNull(tourPackage);
    assertTrue(tourPackage.getToursByCategory().isEmpty());
}
```

In this way, we have a coverage of 100% of the methods (10 of 10) and 96% of the lines (24 of 25) of the SlingModel, which should be more than enough to consider the Unit testing as complete.

## Wrapping Up

Errors are part of human nature, so they cannot be eliminated but avoided using techniques like unit testing.

Developers can create unit tests in AEM using the io.wcm.testing.mock.aem library, as it provides a way to Mock almost the full AEM Context, allowing developers to create tests inside an entirely mocked AEM ecosystem.

However, what's your solution if you don’t have a team of developers with AEM experience? Oshyn is an experienced AEM partner and can help with every aspect of AEM, from development and implementation to continuous maintenance. Contact us to find out how we can help with your AEM needs.

### Further Reading

- Unit testing | Adobe Experience Manager
- Ask the AEM Community Expert Session: Unit Testing with AEM Mocks
- What's new in AEM Mocks
