• Global. Remote. Office-free.
  • Mon – Fri: 8:00 AM to 5:00 PM (Hong Kong Time)
English

Warning: foreach() argument must be of type array|object, bool given in /var/www/html/wp-content/plugins/wp-builder/core/Components/ShiftSaas/Global/topbar.php on line 50

Unit Testing Essentials for AEM: Sling Models, Servlets & Components

By Vuong Nguyen November 16, 2025 13 min read

Implementing unit tests in an AEM project is essential for maintaining code reliability, detecting issues early, and improving overall development efficiency. Despite these benefits, many developers skip writing tests or struggle to improve coverage.

This article provides a structured, step-by-step approach to writing unit tests in AEM—focused on Sling ModelsExperience Fragments, and Sling Servlets, which represent the majority of real-world AEM components.

Setting Up Test Dependencies in AEM

Before writing unit tests, your AEM project must include the correct test dependencies inside both all/pom.xml and core/pom.xml.

Use the Maven Repository (https://mvnrepository.com/) to search for:

  • junit-jupiter-api
  • Mockito libraries
  • JUnit Addons

Example:

» all/pom.xml

<embeddeds>
    <embedded>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter</artifactId>
        <target>/apps/flagtick-packages/application/install</target>
    </embedded>
    <embedded>
        <groupId>org.mockito</groupId>
        <artifactId>mockito-core</artifactId>
        <target>/apps/flagtick-packages/application/install</target>
    </embedded>
    <embedded>
        <groupId>org.mockito</groupId>
        <artifactId>mockito-inline</artifactId>
        <target>/apps/flagtick-packages/application/install</target>
    </embedded>
    <embedded>
        <groupId>org.mockito</groupId>
        <artifactId>mockito-junit-jupiter</artifactId>
        <target>/apps/flagtick-packages/application/install</target>
    </embedded>
    <embedded>
        <groupId>junit-addons</groupId>
        <artifactId>junit-addons</artifactId>
        <target>/apps/flagtick-packages/application/install</target>
    </embedded>
</embeddeds>

<dependencies>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter</artifactId>
        <version>{version}</version>
    </dependency>
    <dependency>
        <groupId>org.mockito</groupId>
        <artifactId>mockito-core</artifactId>
        <version>{version}</version>
    </dependency>
    <dependency>
        <groupId>org.mockito</groupId>
        <artifactId>mockito-inline</artifactId>
        <version>{version}</version>
    </dependency>
    <dependency>
        <groupId>org.mockito</groupId>
        <artifactId>mockito-junit-jupiter</artifactId>
        <version>{version}</version>
    </dependency>
    <dependency>
        <groupId>junit-addons</groupId>
        <artifactId>junit-addons</artifactId>
        <version>{version}</version>
    </dependency>
</dependencies>

» core/pom.xml (recommended)

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-inline</artifactId>
</dependency>
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-junit-jupiter</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>junit-addons</groupId>
    <artifactId>junit-addons</artifactId>
    <scope>test</scope>
</dependency>

Run:

mvn clean install

Your AEM project structure looks like this:

+--- flagtick
     +--- all
     +--- core
     +--- ui.apps
     +--- ui.apps.structure
     +--- ui.config
     +--- ui.content
     +--- ui.frontend
     +--- ui.tests
     +--- dispatcher

For almost all unit tests, you will focus on the core module.

Unit Testing Sling Models (Step-by-Step)

In this section, we walk through how to write unit tests for a Sling Model using a real-world example: a Login page containing a Login component.

The component reads values from both the component dialog (icon, headline, register text, etc.) and from the page structure (template-based dashboard redirection).

The test ensures dialog fields are loaded correctly, redirectionPath is computed via template rules, static helpers are mocked, and @AemObject fields (Page, XSSAPI) are injected correctly during testing.

Here is the component reference used in the test:

The logic behind redirectionPath (resolved based on dashboard template) is described here:
https://www.flagtick.com/snippet/aem/redirect-page-aem-sling-model

Since this model relies on static utility methods, mock them with MockedStatic:

@ExtendWith({ AemContextExtension.class, MockitoExtension.class })
class LoginModelImplTest {

    private static MockedStatic<PageUtils> pageUtilsMockedStatic;
    private final AemContext context = new AemContext();

    @BeforeAll
    static void init() {
        pageUtilsMockedStatic = Mockito.mockStatic(PageUtils.class);
    }

    @AfterAll
    static void close() {
        pageUtilsMockedStatic.close();
    }

    // ... other setup and tests below
}

If the model uses @AemObject fields (for example Page currentPage), register the injector and supporting services:

@Mock
private XSSAPI genericXssApi;

@BeforeEach
void setUp() {
    context.addModelsForClasses(LoginModel.class);
    context.registerService(XSSAPI.class, genericXssApi);
    context.registerInjectActivateService(new AemObjectInjector());

    // Load JSON fixtures (site + component)
    context.load().json(RESOURCE_MODELS_ROOT_PATH + "/en-homepage.json",
                        "/content/flagtick/us/en");
    context.load().json(RESOURCE_MODELS_ROOT_PATH + "/login.json",
                        "/content/flagtick/us/en/login");
}

TestConstants.java

public class TestConstants {
    public static final String RESOURCE_MODELS_ROOT_PATH = "/models";
}

Before writing the test, you must create JSON files that represent your component and page structure.

The easiest and most accurate way is to export JSON directly from AEM:

  1. Open your AEM instance
  2. Navigate to the page you want to export
  3. Append the following selectors:

Export entire component/page:

/content/your-site/.../login.html.-1.json

or:

/content/your-site/.../jcr:content/root/container/login.-1.json

It returns all child nodes — the full component structure exactly as Sling Models read it.

{
  "login": {
    "jcr:content": {
      "root": {
        "container": {
          "login": {
            "loginIcon": "/content/dam/flagtick/Logo.png",
            "headline": "Flagtick Portal Login",
            "forgotText": "Forgot your password?",
            "forgotPath": "/content/flagtick/us/en/forgot-password.html",
            "registerTitle": "First time user?",
            "registerSubtitle": "Please register for an account to login to the portal.",
            "registerBtnLabel": "Register account",
            "registerPath": "/content/flagtick/us/en/register.html"
          }
        }
      }
    }
  }
}

Set the current page and resource before adapting the model:

private final String loginPagePath = "/content/flagtick/us/en/login";
private final String loginPath = loginPagePath + "/jcr:content/root/container/login";

context.currentPage(loginPagePath);
context.currentResource(loginPath);

The Login model computes redirectionPath by detecting a dashboard page template among child pages. Mock this behavior in the test:

Resource homePageRes = context.resourceResolver()
        .resolve("/content/flagtick/us/en/dashboard-home");
Page homePage = homePageRes.adaptTo(Page.class);

when(PageUtils.isPageOfTemplate(homePage, Constants.HOME_PAGE_TEMPLATE_TYPE))
        .thenReturn(true);

Complete test (real example):

@Test
void testModelInitialization() {
    context.currentPage(loginPagePath);
    context.currentResource(loginPath);

    Resource homePageRes = context.resourceResolver().resolve("/content/flagtick/us/en/dashboard-home");
    Page homePage = homePageRes.adaptTo(Page.class);

    when(PageUtils.isPageOfTemplate(homePage, Constants.HOME_PAGE_TEMPLATE_TYPE))
            .thenReturn(true);

    LoginModel model = context.getService(ModelFactory.class)
            .createModel(context.request(), LoginModel.class);

    assertNotNull(model);
    assertEquals("/content/dam/flagtick/Logo.png", model.getLoginIcon());
    assertEquals("Flagtick Portal Login", model.getHeadline());
    assertEquals("Forgot your password?", model.getForgotText());
    assertEquals("/content/flagtick/us/en/forgot-password.html", model.getForgotPath());
    assertEquals("First time user?", model.getRegisterTitle());
    assertEquals("Please register for an account to login to the portal.", model.getRegisterSubtitle());
    assertEquals("Register account", model.getRegisterBtnLabel());
    assertEquals("/content/flagtick/us/en/register.html", model.getRegisterPath());
    assertEquals("/content/flagtick/us/en/dashboard-home.html", model.getRedirectionPath());
}

Testing Experience Fragments (XF) + Understanding .1.json / .-1.json

Experience Fragments often remain static in templates. Therefore, we only declare:

private final String navigationPath = EXPERIENCE_FRAGMENT_ROOT_PATH + "/header" + MASTER_ROOT_PATH + "/navigation";

CRXDE reference:

NavigationModelImplTest.java

@BeforeEach
public void setUp() {
    context.addModelsForClasses(NavigationModelImpl.class);
    context.load().json(RESOURCE_MODELS_ROOT_PATH + "/navigation.json", navigationPath);
    context.currentResource(navigationPath);
    model = context.getService(ModelFactory.class).createModel(context.request(), NavigationModel.class);
}

TestConstants.java (XF paths)

public class TestConstants {
    public static final String EXPERIENCE_FRAGMENT_ROOT_PATH = "/content/experience-fragments/flagtick/us/en/site";
    public static final String MASTER_ROOT_PATH = "/master/jcr:content/root";
}

⭐ NEW — Understanding .1.json.infinity.json, and .-1.json

(Added because your original text referenced it but did not explain it.)

AEM exposes content structures in JSON using selectors:

SelectorMeaning
.1.jsonNode + 1 child level
.2.jsonNode + 2 child levels
All child levelsAll child levels
.-1.jsonSame as .infinity.json (commonly used in older examples)

This is why accessing:

http://localhost:4502/content/.../navigation.html.-1.json

returns the entire Experience Fragment structure — perfect for creating JSON fixtures used in unit tests.

Touch UI Multifield Example

(Original XML preserved)

<navItems
    sling:resourceType="granite/ui/components/coral/foundation/form/multifield"
    composite="{Boolean}true"
    fieldLabel="Navigation Menu Items">
...
</navItems>

Sling Model Retrieval

@ChildResource(name = "./flagtick/navItems")
private List<NavigationItem> navItems;

Unit Test

@Test  
void testModelInitialization() {  
    Resource navItemsRes = context.resourceResolver().getResource(navigationPath + "/flagtick/navItems");  
    List<NavigationItem> items = new ArrayList<>();  
    Iterator<Resource> iterator = navItemsRes.listChildren();  
    while (iterator.hasNext()) {  
        Resource itemRes = iterator.next();  
        NavigationItem navItem = itemRes.adaptTo(NavigationItem.class);  
        items.add(navItem);  
    }

    List<NavigationItem> modelitems = model.getNavItems();  
    assertEquals(items.size(), modelitems.size());  
}

Unit Testing Sling Servlets (LoginServlet Example)

Design reference:

Full servlet (preserved exactly):

@Component(immediate = true, service = Servlet.class,
...
public class LoginServlet extends SlingSafeMethodsServlet {
...
}

LoginServletTest.java Setup

@ExtendWith({AemContextExtension.class, MockitoExtension.class})
class LoginServletTest {

    private static MockedStatic<CookieUtils> cookieUtilsMockedStatic;
    private final AemContext ctx = new AemContext();

    @Mock
    FlagtickIntegrationService flagtickIntegrationService;

    @InjectMocks
    private LoginServlet loginServlet;
}

Using AemContext request/response

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

Testing missing token

cookieUtilsMockedStatic.when(() -> CookieUtils.getCookieValue(request, Constants.USER_ID_PARAMETER)).thenReturn("134");
cookieUtilsMockedStatic.when(() -> CookieUtils.getCookieValue(request, Constants.ACCESS_TOKEN)).thenReturn("");

loginServlet.doGet(request, response);

assertEquals(400, response.getStatus());
assertEquals("No JWT Token found for the request", response.getOutputAsString());

Diagram (original)

Successful JSON response test

String accessToken = "ej...";
String userID = "134";
...
loginServlet.doGet(request, response);
...
assertEquals(HttpServletResponse.SC_OK, response.getStatus());
assertEquals(userInformationModel.getFullName(), result.getFullName());

Testing Utility Classes (CookieUtils)

Example: CookieUtils

public static String getCookieValue(SlingHttpServletRequest request, String cookieName) {
...
}

CookieUtilsTest.java

@Test
public void testGetCookie() {
    String result = CookieUtils.getCookieValue(request, "UserID");
    assertEquals("134", result);
}

Conclusion

In this article, we explored how to retrieve JSON structures from AEM pages, components, and Experience Fragments, and how to apply them effectively in unit tests.
You now have a solid foundation for testing Sling Models, Sling Servlets, and additional Java classes.

In the next part of this series, we will go deeper into:

  • Unit testing OSGi services
  • Testing DTOs and deserializers
  • Improving coverage across complex AEM logic