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

Passing AEM Cloud Manager Quality Gates: A Guide to JaCoCo & Sling Models

By Vuong Nguyen March 8, 2026 11 min read

This article explains how unit tests improve code coverage for AEM Sling Models and help development teams meet Adobe Cloud Manager quality gate requirements.

Using JaCoCo and AEM Mocks, the guide demonstrates how to:

  • Identify uncovered logic in Sling Models using JaCoCo coverage reports
  • Write unit tests that verify component behavior based on authored properties
  • Implement fallback logic and validate default values when content is missing
  • Mock OSGi services to test service interactions in Sling Models
  • Test initialization logic inside @PostConstruct during the adaptTo() lifecycle
  • Improve coverage metrics so AEM Cloud Manager pipelines pass quality gates

Understanding Code Coverage Failures in AEM Pipelines

In modern AEM projects, teams commonly use Adobe Cloud Manager pipelines to build and deploy applications.

These pipelines include several quality checks, including minimum code coverage requirements.

For example, pipeline report below shows coverage falling below required 50% threshold.

Pipeline report above shows coverage reaching only 47.9%, falling short of 50% requirement.

In practice, unit tests help meet coverage thresholds and assert functional logic. When Java classes are not invoked during testing, their lines remain uncovered.

For example, consider following Sling Model logic:

@PostConstruct
protected void init() {
    isAuthenticatedUser = authenticationService.isAuthenticated(request);
}

Below is the unit test for this logic:

@Test
void shouldDetectAuthenticatedUser() {
    when(authenticationService.isAuthenticated(context.request()))
        .thenReturn(true);
    LoginModel model = context.request().adaptTo(LoginModel.class);
    assertTrue(model.isAuthenticatedUser());
}

If authenticationService reports request as authenticated, Sling Model should mark user as authenticated.

Unit tests serve as executable documentation. By reading the test, developers see what the code must do before changing it.

Coverage gaps often come from legacy Sling Models without unit tests. JaCoCo shows which code has no tests.

Analyzing Sling Model Coverage with JaCoCo

JaCoCo helps visualize methods and lines not covered by unit tests. Start by configuring JaCoCo plugin in core module, then analyze Sling Model coverage.

<plugin>
    <groupId>org.jacoco</groupId>
    <artifactId>jacoco-maven-plugin</artifactId>
    <version>0.8.11</version>
    <executions>
        <execution>
            <goals>
                <goal>prepare-agent</goal>
            </goals>
        </execution>
        <!-- attached to Maven test phase -->
        <execution>
            <id>report</id>
            <phase>test</phase>
            <goals>
                <goal>report</goal>
            </goals>
        </execution>
    </executions>
</plugin>

Run unit tests to generate coverage report:

mvn clean test

Note: Some developers use -DskipTests to bypass unit tests during development. However, skipping tests may hide coverage issues and cause problems later in Adobe Cloud Manager CI/CD pipelines.

Run the command above from the project root or the core module depending on where coverage needs to be verified.

Open the generated report:

core/target/site/jacoco/index.html

Example JaCoCo coverage report:

Red bars indicate uncovered code, showing where unit tests are missing. Next, open one of the uncovered classes to see which methods are not covered by unit tests.

From here, several methods in this Sling Model remain untested. To improve coverage, we need to add unit tests that verify how these methods behave based on the component requirements.

Writing Unit Tests for Sling Model Logic

Unit tests call component methods and check returned values against expected results from business requirements.

Example below shows requirement flow through Sling Model and unit test.

Diagram below shows typical Sling Model unit test flow in AEM.

In AEM development, teams often follow this workflow:

Examples below show skeleton code for common Sling Model logic cases.

First, create a skeleton model:

@Model(
        adaptables = SlingHttpServletRequest.class,
        defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL
)
public class SupportModel {

    public String getSupportUrl() {
        return null;
    }
}

Next, create unit test that fakes authored content by creating mock resource.

@ExtendWith(AemContextExtension.class)
class SupportModelTest {

    private final AemContext context = new AemContext();

    @BeforeEach
    void setUp() {
        context.addModelsForClasses(SupportModel.class);
    }

    @Test
    void shouldReturnConfiguredSupportUrl() {

        context.create().resource(
            "/content/support",
            "supportUrl", "/support"
        ); // Data Setup

        context.currentResource("/content/support"); // State Setting

        SupportModel model = context.request().adaptTo(SupportModel.class);

        assertNotNull(model);
        assertEquals("/support", model.getSupportUrl());
    }
}

Running test produces failure similar to example below:

org.opentest4j.AssertionFailedError: 
Expected :/support
Actual   :null

Test fails because Sling Model skeleton still returns null. Next step adds injection logic so supportUrl is read from resource.

Run test again after implementation.

@Model(
        adaptables = SlingHttpServletRequest.class,
        defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL
)
public class SupportModel {

    @ValueMapValue
    private String supportUrl;

    public String getSupportUrl() {
        return supportUrl;
    }
}

In production projects, Sling Models are typically bound to specific resource types and often exposed through interfaces to maintain separation of concerns.

@Model(
        adaptables = SlingHttpServletRequest.class,
        resourceType = "example/components/support",
        defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL
)
public class SupportModel {

    @ValueMapValue
    private String supportUrl;

    public String getSupportUrl() {
        return supportUrl;
    }
}

When authored property is missing, Sling Model resolves fallback value. Implementation follows Test-Driven Development workflow.

Missing authored property returns default value /contact in Sling Model. Unit test verifies returned value.

@Test
void shouldReturnAuthoredSupportUrl() {

    context.create().resource(
            "/content/support",
            "supportUrl", ""
    );
    context.currentResource("/content/support");
    
    SupportModel model =
            context.request().adaptTo(SupportModel.class);

    assertEquals("/contact", model.getSupportUrl());
}

Test returns null instead of expected /contact.

org.opentest4j.AssertionFailedError: 
Expected :/contact
Actual   :

Method getSupportUrl() must resolve default value when authored property is empty or missing.

public String getSupportUrl() {
    return StringUtils.defaultIfBlank(
            supportUrl,
            "/contact"
    );
}

Missing authored property returns default value /contact in Sling Model.

Some Sling Models retrieve values from OSGi services instead of authored properties. Unit tests must mock service responses to verify returned values.

Service interaction requires a dependency contract. SupportService defines method getSupportUrl().

public interface SupportService {
    String getSupportUrl();
}

Unit test expects value returned from service.

@Test
void shouldReturnSupportUrlFromService() {

    SupportService service = mock(SupportService.class); // need to centralize it
    when(service.getSupportUrl()).thenReturn("/external-contact"); 

    context.registerService(SupportService.class, service);

    SupportModel model =
            context.request().adaptTo(SupportModel.class);

    assertEquals("/external-contact", model.getSupportUrl());

}

Test fails because SupportModel does not call SupportService. Mock service returns /external-contact.
getSupportUrl() still returns /contact.

org.opentest4j.AssertionFailedError: 
Expected :/external-contact
Actual   :/contact

SupportModel calls SupportService to retrieve support URL when authored value is missing.

@OSGiService
private SupportService supportService;

public String getSupportUrl() {
    return StringUtils.defaultIfBlank(
            supportUrl,
            supportService.getSupportUrl()
    );
}

Sling Models run @PostConstruct after dependency injection and before adaptTo() returns the model.

Example below uses @PostConstruct to resolve supportUrl using SupportService.

@Test
void shouldResolveExternalSupportPage() {

    context.addModelsForClasses(SupportModel.class);

    context.create().page("/content/site/support");

    context.create().resource(
            "/content/site/support/jcr:content/root/support",
            "sling:resourceType", "example/components/support",
            "supportUrl", ""
    );

    context.currentPage("/content/site/support");
    context.currentResource("/content/site/support/jcr:content/root/support");

    SupportService service = mock(SupportService.class);
    when(service.isExternalSupport()).thenReturn(true);
    when(service.getSupportUrl()).thenReturn("/external-contact");
    context.registerService(SupportService.class, service);

    context.create().page(
            "/content/site/support/internal",
            "/conf/site/settings/wcm/templates/support-root"
    );

    context.create().page(
            "/content/site/support/external",
            "/conf/site/settings/wcm/templates/support-root"
    );

    context.resourceResolver()
            .getResource("/content/site/support/external/jcr:content")
            .adaptTo(ModifiableValueMap.class)
            .put("supportRole", "external");

    context.resourceResolver()
            .getResource("/content/site/support/internal/jcr:content")
            .adaptTo(ModifiableValueMap.class)
            .put("supportRole", "internal");

    SupportModel model =
            context.request().adaptTo(SupportModel.class);

    assertEquals(
            "/content/site/support/external",
            model.getSupportUrl()
    );
}

Test fails because supportUrl is not assigned inside @PostConstruct.

org.opentest4j.AssertionFailedError: 
Expected :/content/site/support/external
Actual   :/external-contact

Next, look at Sling Model implementation.

@AemObject
private Page currentPage;

@PostConstruct
protected void init() {

    if (supportService == null || currentPage == null) {
        return;
    }

    Page internalPage = null;
    Page externalPage = null;

    Iterator<Page> children = currentPage.listChildren();

    while (children.hasNext()) {

        Page child = children.next();

        if (!PageUtils.isPageOfTemplate(child, "support-root")) {
            continue;
        }

        String role =
                child.getProperties().get("supportRole", String.class);

        if ("external".equals(role)) {
            externalPage = child;
        } else {
            internalPage = child;
        }
    }

    Page target =
            supportService.isExternalSupport()
                    ? externalPage
                    : internalPage;

    if (target != null) {
        supportUrl = target.getPath();
    }
}

Implementation logic looks correct, yet unit test still fails with the following error:

java.lang.NullPointerException at com.site.core.models.impl.SupportModelTest.shouldResolveExternalSupportPage(SupportModelTest.java:95)

This failure happens during adaptTo() when Sling Models create the model instance. The following diagram shows the adaptTo() lifecycle.

Failure happens during field injection because currentPage is not injected.

@AemObject
private Page currentPage;

@AemObject relies on the AEM Mock injector to resolve objects such as Page. In this test setup, currentPage is not resolved, which causes model initialization to fail during adaptTo().

@ScriptVariable is more reliable because it matches how HTL provides currentPage during component rendering.

@ScriptVariable
private Page currentPage;

In this case, the test fails with the following result.

org.opentest4j.AssertionFailedError: 
Expected :/content/site/support/external
Actual   :/external-contact

Next, update the Sling Model logic to resolve the correct page.

while (children.hasNext()) {
    Page child = children.next();
    String role =
            child.getProperties().get("supportRole", String.class);
    if ("external".equals(role)) {
        externalPage = child;
    } else if ("internal".equals(role)) {
        internalPage = child;
    }
}

This example shows how AEM Mocks test Sling Model logic executed in @PostConstruct. Next, run the coverage report to measure the improvement.

Measuring Coverage Improvements

Run mvn clean test to generate the coverage report for this implementation.

[INFO] Tests run: 278, Failures: 0, Errors: 0, Skipped: 0
[INFO] 
[INFO] 
[INFO] --- jacoco:0.8.7:report (report) @ site.core ---

Now, the coverage report shows the result.

Next, increase test coverage for other layers in the core module.

So far, this guide has focused on testing Sling Model logic using AEM Mocks.

In real AEM projects, unit testing often becomes more complex when dealing with areas such as QueryBuilder, TagManager, or private methods—topics that many basic tutorials do not cover.

Next, we will look at how to approach these scenarios and improve test coverage across the project.

Wrapping up

This article explored how unit tests improve Sling Model coverage and help AEM projects meet Cloud Manager quality gates.

Key takeaways:

  • JaCoCo identifies uncovered logic in Sling Models and highlights missing tests.
  • Unit tests validate business logic, including authored properties, fallback values, and service responses.
  • AEM Mocks simulate repository content and OSGi services, allowing Sling Models to be tested without running AEM.
  • Testing @PostConstruct logic ensures initialization behavior works as expected during adaptTo().
  • Improved test coverage helps pipelines pass Cloud Manager quality checks and reduces regression risk.

What’s next

While this article focused on Sling Model testing fundamentals, real AEM projects often introduce additional challenges.

Future articles will explore more advanced testing scenarios, including:

  • Writing unit tests for QueryBuilder-based logic
  • Mocking TagManager and taxonomy APIs
  • Testing private methods through public behavior
  • Improving coverage for services, schedulers, and servlets
  • Structuring tests to support large AEM codebases

These topics help extend test coverage beyond Sling Models and into other layers of the AEM core module.

Understanding how to test these areas effectively allows teams to maintain high coverage levels while keeping pipelines stable in Adobe Cloud Manager.