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.
When agents use itUse 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.
Install
Copilot (VS Code, Visual Studio, Copilot CLI and github.com) reads skills from the repository — commit them so the whole team gets them.
npx skills add AGCO-Global/org-skills --skill aem-testing -a github-copilot
# or with the org installer (adds .github/skills/aem-testing):
npx -y github:AGCO-Global/org-skills add skill aem-testing
Uses the open skills CLI. Works with Claude Code, Codex, Cursor, Copilot, Gemini CLI, OpenCode, Windsurf and 60+ others — it asks which agent to install into.
Installs the aem-quality-skills plugin, which bundles all AEM / Quality skills and keeps them updated.
/plugin marketplace add AGCO-Global/org-skills
/plugin install aem-quality-skills@org-skills
# or just this skill, in this repository:
npx skills add AGCO-Global/org-skills --skill aem-testing -a claude-code
Use Download .zip above, then upload it under Customize → Skills → Upload skill. Team and Enterprise admins can sync this repo as a plugin marketplace instead.
Installs into .agents/skills/, which Codex reads.
npx skills add AGCO-Global/org-skills --skill aem-testing -a codex
Installs into Cursor's skills folder.
npx skills add AGCO-Global/org-skills --skill aem-testing -a cursor
Installs into Gemini CLI's skills folder.
npx skills add AGCO-Global/org-skills --skill aem-testing -a gemini-cli
Any tool with rules, instructions or custom prompts: use Copy SKILL.md above and paste it in. It is plain Markdown.
Commands use your normal git sign-in to GitHub, so they work while the repository is private. Node.js 20+ required.
Skill contents
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.
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
@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.
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
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
Layer decision: which layer each behaviour is tested at, and why not lower.
Test classes with JUnit 5 + AEM Mocks, named <ClassUnderTest>Test, one scenario per test method, descriptive names.
Fixtures: JSON content files and policy mappings, covering authored/empty/invalid dialog states.
Integration/UI tests only for flows that need a running AEM, with setup/teardown of test content.
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.
---
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.