---
name: aem-testing
description: Design and write tests for Adobe Experience Manager projects — JUnit 5 unit tests with AEM Mocks for Sling Models, servlets and OSGi services, integration tests with AEM testing clients, UI tests, front-end Jest tests, and Cloud Manager quality gates. Use this whenever the user writes or fixes a test for AEM code, mentions AemContext, AemContextExtension, io.wcm aem-mock, ResourceResolverType (JCR_MOCK, RESOURCERESOLVER_MOCK, JCR_OAK), context.load().json, registerInjectActivateService, MockSlingHttpServletRequest, Mockito in AEM, the it.tests or ui.tests module, Cypress/WebdriverIO/Selenium in Cloud Manager, JaCoCo coverage, SonarQube or OakPAL failures, a pipeline blocked by the code quality gate, or asks how to test an HTL component, dialog or content policy. Also apply it when reviewing AEM test code or planning a test strategy for an AEM project.
metadata:
  technology: AEM
  type: testing
---

# AEM Testing

Most AEM logic lives in Sling Models, services and servlets — test it there, fast, with AEM Mocks and realistic JSON content; reserve slow, flaky-prone browser and instance tests for what only a running AEM can prove. A test that needs a live instance to check a null-check is in the wrong layer.

## 1. What to test at which layer

| Layer | Tool | Tests | Runs |
|---|---|---|---|
| Unit (most tests) | JUnit 5 + AEM Mocks + Mockito (`core`) | Sling Models, services + configs, servlets, filters, job consumers, workflow steps | Every build; Cloud Manager coverage |
| Front-end unit | Jest (`ui.frontend`) | JS/TS modules, client-side component logic | Maven build via the front-end plugin |
| Content/package | OakPAL, Cloud Manager rules, filter checks | Package filters, `/apps` vs `/content` separation, index definitions | Code quality step |
| Integration | AEM testing clients (`it.tests`) | Servlet endpoints, rendered component markup, service users/ACLs, replication | Against a deployed environment |
| UI / E2E | Cypress, WebdriverIO, Selenium, Playwright (`ui.tests`) | Critical author and visitor journeys | Cloud Manager custom UI testing |
| Performance | JMeter / k6 / Gatling, Lighthouse | Throughput through CDN/Dispatcher, page weight | Stage, scheduled, not per commit |

Push every assertion as far down this table as it can go.

## 2. AEM Mocks — choose the ResourceResolverType

| Type | Use when | Trade-off |
|---|---|---|
| `RESOURCERESOLVER_MOCK` (default) | Models, servlets, services using the Sling API only | Fastest; no JCR `Node`/`Session`, no queries |
| `JCR_MOCK` | Code adapts to `Node`/`Session` | In-memory JCR; queries return only what you stub via `MockJcr` |
| `JCR_OAK` | Real queries, node types, ACLs, versioning | Needs the Oak mock dependency; seconds per context — isolate in few test classes |
| `NONE` | No repository at all | Pure OSGi wiring tests |

Pick the lightest type that exercises the code path; switching everything to `JCR_OAK` to make one test pass hides design problems and slows the suite.

## 3. Testing Sling Models

```java
@ExtendWith({AemContextExtension.class, MockitoExtension.class})
class TeaserModelTest {
    private final AemContext context = new AemContext();   // RESOURCERESOLVER_MOCK

    @BeforeEach
    void setUp() {
        context.addModelsForClasses(TeaserModel.class);
        context.load().json("/com/acme/core/models/TeaserModelTest.json", "/content/acme");
    }

    @Test
    void usesPageTitleWhenTitleNotAuthored() {
        context.currentResource("/content/acme/en/jcr:content/root/teaser-no-title");
        TeaserModel model = context.request().adaptTo(TeaserModel.class);
        assertNotNull(model);
        assertEquals("English Home", model.getTitle());
    }
}
```

- **One JSON fixture per test class**, next to it in `src/test/resources`, containing one resource per scenario (authored, empty, partially authored, invalid). This is how you cover **dialog** fields: every dialog field should appear authored and missing somewhere in the fixture.
- **Content policies**: `context.contentPolicyMapping("acme/components/teaser", "showDescription", true)` rather than hand-building `/conf` trees.
- Adapt from the **same adaptable** as production (`request` vs `resource`); a model that works from a resource in tests can return null from the request in HTL.
- Test the model's getters, not HTL. HTL rendering has no practical unit-test harness — keep logic out of HTL so there's nothing left to test there; verify markup in integration tests if it matters.
- For Core Components delegation, also add the Core Components test dependency and register the delegate's models.

## 4. OSGi services with configuration

```java
@Test
void appliesConfiguredTimeout() {
    context.registerService(HttpClientFactory.class, httpClientFactoryMock);
    PriceService service = context.registerInjectActivateService(new PriceServiceImpl(),
            Map.of("endpoint", "https://test.invalid", "timeout", 500));
    …
}
```

- `registerInjectActivateService` runs real DS injection and `@Activate`; mandatory references must be registered first or it fails — that failure is a useful test of your wiring.
- Mock outbound dependencies (HTTP clients, `JobManager`, `Replicator`) with Mockito and verify the interactions; use real AEM Mocks objects (resources, pages, `ResourceResolver`) instead of mocking them.
- Service users: service logins through the mocked `ResourceResolverFactory` see the same mock repository — test the logic, and verify ACLs in integration tests.

## 5. Servlets, filters, jobs, workflow steps

```java
context.currentResource("/content/acme/en/jcr:content/product");
context.requestPathInfo().setSelectorString("prices");
context.requestPathInfo().setExtension("json");
context.request().setQueryString("currency=EUR");
servlet.doGet(context.request(), context.response());
assertEquals(200, context.response().getStatus());
assertEquals("application/json", context.response().getContentType());
JsonNode body = new ObjectMapper().readTree(context.response().getOutputAsString());
```

- Cover the error paths: bad selector/parameter → 400, missing resource → 404, downstream failure → 5xx with no stack trace in the body.
- `JobConsumer`: build a mocked `Job` with properties, assert `JobResult` for success, retryable failure and permanent failure.
- `WorkflowProcess`: mock `WorkItem`, `WorkflowData` (payload path) and `MetaDataMap` for process args; run against AEM Mocks content.
- Filters: invoke `doFilter` with a Mockito `FilterChain` and verify it was (or wasn't) called.

## 6. Integration tests (`it.tests`)

- Use the AEM testing clients (`aem-cloud-testing-clients`) with author/publish class rules; Cloud Manager runs this module in the custom functional testing step against the deployed environment.
- Test what mocks cannot: servlet resolution and Dispatcher-facing URLs, ACLs and service users, repoinit results, replication to publish, rendered markup of key components.
- Create test content under a unique, test-owned path and delete it in teardown; never depend on authored production content.
- Poll with timeouts for async effects (replication, jobs) — no fixed sleeps.

## 7. UI tests (`ui.tests`) and front-end tests

- Cloud Manager builds `ui.tests` into a Docker image and runs it against the environment, passing URLs and credentials as environment variables (e.g. `AEM_AUTHOR_URL`, `AEM_PUBLISH_URL`); read them, never hardcode hosts. The module must opt in (a `testing.properties` with `ui-tests.version=1` in current archetypes) and publish reports where the pipeline expects them.
- Keep the suite small: log in to author, author a component, publish, verify on publish; key visitor journeys. Use stable `data-*` selectors, not generated CSS classes.
- `ui.frontend`: Jest (with jsdom) for component JS; run in the Maven build so failures break the pipeline.

## 8. Cloud Manager quality gates

- **Code coverage** (JaCoCo) under the threshold (50 % on Cloud Service at time of writing) is an *Important* failure — the pipeline pauses for override. Treat it as a floor; aim for ~80 % on models/services, excluding generated or trivial classes explicitly in JaCoCo, not by writing assertion-free tests.
- **SonarQube** reliability/security ratings and **AEM-specific rules** (e.g. admin sessions, resolvers not closed, `Thread.sleep`, deprecated APIs) can be *Critical* and fail the build — fix, don't suppress; any suppression carries a comment.
- **OakPAL/package rules**: mixed mutable/immutable packages, `/libs` overlays of non-overlayable nodes, bad index definitions, config in wrong folders.
- Reproduce locally: run the same Maven build, the AEM analyser plugin, and a SonarQube scan with the project's quality profile before pushing.

## 9. Deterministic tests and fixtures

- Inject a `Clock` (or time provider) into services; never assert on `new Date()`. Fix `Locale` and `TimeZone` in tests that format.
- No network, no real instance, no `Thread.sleep` in unit tests; no order dependence — a fresh `AemContext` per test (the extension does this).
- Fixture JSON is minimal and readable: only properties the code reads, realistic `sling:resourceType` values.
- Performance: script load tests against stage through the CDN, with realistic cache-hit ratios; coordinate before running, and compare against a baseline.

## Deliverable format

1. **Layer decision**: which layer each behaviour is tested at, and why not lower.
2. **Test classes** with JUnit 5 + AEM Mocks, named `<ClassUnderTest>Test`, one scenario per test method, descriptive names.
3. **Fixtures**: JSON content files and policy mappings, covering authored/empty/invalid dialog states.
4. **Integration/UI tests** only for flows that need a running AEM, with setup/teardown of test content.
5. **Quality gate notes**: coverage impact, Sonar/OakPAL rules touched, JaCoCo exclusions justified.

## Anti-patterns to reject

- Mocking `Resource`, `ValueMap` or `ResourceResolver` with Mockito instead of loading JSON into AEM Mocks.
- `JCR_OAK` as the default for every test class; slow suites nobody runs locally.
- Tests without assertions, or asserting only `assertNotNull(model)`, written to pass the coverage gate.
- Business logic in HTL or client-side JS "because the model is hard to test".
- UI tests that check what a unit test could; `Thread.sleep`, hardcoded hosts or credentials in `ui.tests`/`it.tests`.
- Integration tests depending on existing authored content or leaving test content behind.
- Suppressing SonarQube or AEM rules without a written reason; lowering coverage by excluding real code.
- Time-, locale- or order-dependent tests that fail intermittently in the pipeline.
