> For AI agents: the complete documentation index is available at https://nhanbe.id.vn/llms.txt, the full documentation bundle is available at https://nhanbe.id.vn/llms-full.txt.

# Testing an AEM 6.5 LTS Project

## Test pyramid AEM 6.5

| Layer             | Scope                                                 | Runtime        |
| ----------------- | ----------------------------------------------------- | -------------- |
| Unit              | Sling Models, services, utilities + mocked repository | \~100 ms/class |
| Integration-style | repository + OSGi wiring, in-memory JCR               | \~1 s/class    |
| UI                | authoring flows, rendering, Dispatcher                | \~30 s/test    |

Quy tắc: unit test được thì làm unit test. Integration test chỉ cho behavior mà repository mới sinh ra (resource resolution, queries, inheritance). UI test cho flow động vào authoring UI + HTL + Dispatcher end-to-end.

## Tooling (6.5 LTS)

| Purpose               | Tool                                                    | Note                                                                      |
| --------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------- |
| Runner                | JUnit 5 (`junit-jupiter`)                               | `@ExtendWith(AemContextExtension.class)`                                  |
| AEM + Sling mocks     | wcm.io AEM Mocks `io.wcm.testing.mock.aem-junit5`       | De facto standard, bundle Sling Mocks + OSGi Mock + AEM extension         |
| Assert                | AssertJ hoặc plain JUnit                                | AssertJ chaining đọc object sâu hay hơn                                   |
| Mock collaborator     | Mockito                                                 | Chỉ cho external; Sling/OSGi service ưu tiên `AemContext.registerService` |
| HTTP stub             | WireMock                                                | Stub external API                                                         |
| UI tests              | Selenium via `ui.tests` module, hoặc Cypress/Playwright | Hobbes.js deprecated                                                      |
| Dispatcher validation | **AMS Dispatcher SDK `validate.sh`**                    | AMS 6.5, không phải `dispatcher-cloud-optimized` CLI                      |

:::info\[AEM Mocks vs Sling Mocks]
`io.wcm.testing.mock.aem` wrap `SlingContext` (Apache Sling Mocks) và preload AEM classloader: `Page`/`PageManager`, `Tag`/`TagManager`, `com.day.cq.commons.jcr` utilities, Designer stubs. `AemContext` = AEM Mocks extension của `SlingContext`.

Default dùng **AEM Mocks**. Chỉ fall về Sling Mocks cho code thuần Sling/OSGi không đụng `com.day.cq.*`.
:::

## `AemContext` — building block

```java
import io.wcm.testing.mock.aem.junit5.AemContext;
import io.wcm.testing.mock.aem.junit5.AemContextExtension;
import org.apache.sling.resourceresolver.ResourceResolverType;

@ExtendWith(AemContextExtension.class)
class MyModelTest {
  // Default: ResourceResolverType.RESOURCERESOLVER_MOCK
  private final AemContext ctx = new AemContext();
}
```

Các thành phần chính:

| API                                                               | Chức năng                                        |
| ----------------------------------------------------------------- | ------------------------------------------------ |
| `ctx.resourceResolver()`                                          | `ResourceResolver` live theo mock type           |
| `ctx.pageManager()`                                               | `com.day.cq.wcm.api.PageManager` tạo page        |
| `ctx.load()`                                                      | Load JSON/XML/binary vào repo                    |
| `ctx.create()`                                                    | Fluent builder cho resource/page/asset           |
| `ctx.registerService(...)` / `registerInjectActivateService(...)` | Đăng ký mock/real service vào OSGi mock registry |
| `ctx.addModelsForClasses(...)` / `addModelsForPackage(...)`       | Đăng ký Sling Models để adapt                    |
| `ctx.request()` / `ctx.response()`                                | `MockSlingHttpServletRequest/Response`           |
| `ctx.currentResource(path)` / `currentPage(path)`                 | Điều khiển request context                       |

### Chọn `ResourceResolverType`

```java
AemContext ctx = new AemContext(ResourceResolverType.JCR_MOCK);
```

| Type                              | Backend                     | JCR query         | Dùng khi                                   |
| --------------------------------- | --------------------------- | ----------------- | ------------------------------------------ |
| `RESOURCERESOLVER_MOCK` (default) | in-memory map               | ❌                 | 95% Sling Model tests, nhanh nhất (ms)     |
| `JCR_MOCK`                        | fake JCR                    | limited           | Cần `Node.getProperty()` nhưng không query |
| `JCR_OAK`                         | Oak segment store in-memory | ✅ JCR-SQL2, XPath | Dùng QueryBuilder / SQL2                   |
| `JCR_JACKRABBIT`                  | Jackrabbit 2 thật           | ✅                 | Legacy, hiếm                               |

:::warning\[JCR\_OAK boot chậm]
Mỗi class `AemContext(JCR_OAK)` trả 200–500ms startup. Gộp hết query-dependent tests vào 1 class, đừng rải rác khắp suite.
:::

## Load test content

### JSON fixtures

```java
@BeforeEach
void setUp() {
  ctx.load().json("/content/teaser.json", "/content/site/en");
  ctx.addModelsForClasses(TeaserModel.class);
}
```

`src/test/resources/content/teaser.json`:

```json
{
  "jcr:primaryType": "cq:Page",
  "jcr:content": {
    "jcr:primaryType": "cq:PageContent",
    "sling:resourceType": "myproject/components/page",
    "cq:template": "/conf/myproject/settings/wcm/templates/page",
    "root": {
      "jcr:primaryType": "nt:unstructured",
      "teaser": {
        "jcr:primaryType": "nt:unstructured",
        "sling:resourceType": "myproject/components/teaser",
        "title": "My Teaser",
        "link": "/content/site/page"
      }
    }
  }
}
```

Arg 1 relative tới `src/test/resources`. Arg 2 = root path mount trong test repository.

### FileVault XML + binary

```java
// /content/teaser-tree/.content.xml exported từ CRXDE
ctx.load().filevault("/content/teaser-tree", "/content/site/en");

ctx.load().binaryFile("/dam/hero.jpg",
    "/content/dam/images/hero.jpg/jcr:content/renditions/original");
```

## Patterns

### Sling Model

```java
@ExtendWith(AemContextExtension.class)
class TeaserModelTest {
  private final AemContext ctx = new AemContext();

  @BeforeEach
  void setUp() {
    ctx.load().json("/content/teaser.json", "/content/site/en");
    ctx.addModelsForClasses(TeaserModel.class);
  }

  @Test
  void readsTitleAndLink() {
    Resource r = ctx.resourceResolver()
        .getResource("/content/site/en/jcr:content/root/teaser");
    TeaserModel model = r.adaptTo(TeaserModel.class);

    assertEquals("My Teaser", model.getTitle());
    assertEquals("/content/site/en/page", model.getLink());
  }
}
```

### Model cần request (selectors, params)

```java
@Test
void readsSelector() {
  ctx.currentResource("/content/site/en/jcr:content/root/teaser");
  ctx.requestPathInfo().setSelectorString("print");

  TeaserModel model = ctx.request().adaptTo(TeaserModel.class);
  assertTrue(model.isPrintVariant());
}
```

### OSGi service (register-activate)

```java
@BeforeEach
void setUp() {
  ctx.registerInjectActivateService(new FeatureServiceImpl(),
      "enabled", true,
      "threshold", 10);
}

@Test
void featureIsEnabled() {
  FeatureService service = ctx.getService(FeatureService.class);
  assertTrue(service.isEnabled());
  assertEquals(10, service.getThreshold());
}
```

`registerInjectActivateService` wire `@Reference` từ mock OSGi registry, activate với config, đăng ký mọi interface impl.

### Service-user resolver

AEM Mocks pre-register sẵn `ResourceResolverFactory`, sub-service check **permissive** — mọi request đều thành công.

```java
@Test
void writesWithServiceUser() throws Exception {
  var service = ctx.registerInjectActivateService(new MyWriterService());
  service.writeMessage("/content/site/en", "Hello");

  Resource written = ctx.resourceResolver().getResource("/content/site/en/message");
  assertEquals("Hello", written.getValueMap().get("value", String.class));
}
```

:::warning\[Push đúng chỗ]
AEM Mocks bỏ qua sub-service mapping correctness. Cái này phải verify ở **integration test** trên real SDK instance, không phải unit test. Đang test logic write, không test osgi:repoinit config.
:::

### Servlet

```java
@ExtendWith(AemContextExtension.class)
class GreetServletTest {
  private final AemContext ctx = new AemContext();
  private GreetServlet servlet;

  @BeforeEach
  void setUp() {
    servlet = ctx.registerInjectActivateService(new GreetServlet());
  }

  @Test
  void respondsWithJson() throws Exception {
    ctx.request().setParameterMap(Map.of("name", "Ada"));
    servlet.doGet(ctx.request(), ctx.response());

    assertEquals(200, ctx.response().getStatus());
    assertEquals("application/json", ctx.response().getContentType());
    assertTrue(ctx.response().getOutputAsString().contains("\"greeting\":\"Hello, Ada\""));
  }
}
```

### JCR-SQL2 query

```java
@ExtendWith(AemContextExtension.class)
class FragmentFinderTest {
  private final AemContext ctx = new AemContext(ResourceResolverType.JCR_OAK);

  @BeforeEach
  void setUp() {
    ctx.load().json("/content/fragments.json", "/content/experience-fragments");
  }

  @Test
  void findsFragmentsByResourceType() {
    FragmentFinder finder = ctx.registerInjectActivateService(new FragmentFinder());
    List<Resource> results = finder.findByType("myproject/components/fragment");
    assertEquals(3, results.size());
  }
}
```

### Context-Aware Configuration

```java
@BeforeEach
void setUp() {
  MockContextAwareConfig.registerAnnotationClasses(ctx, SiteConfig.class);
  MockContextAwareConfig.writeConfiguration(ctx, "/content/site/en",
      SiteConfig.class, "analyticsId", "GA-TEST-123");
}
```

Đọc qua `request().adaptTo(ConfigurationBuilder.class).as(SiteConfig.class)`.

### Stub HTTP với WireMock

```java
@ExtendWith({AemContextExtension.class, WireMockExtension.class})
class WeatherServiceTest {
  private final AemContext ctx = new AemContext();

  @RegisterExtension
  static WireMockExtension wm = WireMockExtension.newInstance()
      .options(wireMockConfig().dynamicPort())
      .build();

  @BeforeEach
  void setUp() {
    wm.stubFor(get("/forecast?city=Berlin")
        .willReturn(okJson("{\"tempC\":12}")));
    ctx.registerInjectActivateService(new WeatherServiceImpl(),
        "apiBaseUrl", wm.baseUrl());
  }

  @Test
  void parsesForecast() {
    WeatherService svc = ctx.getService(WeatherService.class);
    assertEquals(12, svc.getTemperature("Berlin"));
  }
}
```

## `adaptTo` null checklist

- `addModelsForClasses(Foo.class)` — Sling Models không tự scan trong test.
- `@Model(adaptables = ...)` — adapt từ `Resource` mà model khai `SlingHttpServletRequest.class` thì fail.
- Missing `@ValueMapValue` không default → adapt fail silently. Kiểm log: wcm.io ghi reason ở DEBUG.

## Integration tests (UI)

AEM Maven Archetype scaffold `ui.tests` module. Trong AMS 6.5 chạy Selenium IT qua `-PintegrationTests`, không phải Cloud Manager custom pipeline như aaaCS.

```bash
mvn verify -PintegrationTests \
  -Dsling.it.instance.url=http://localhost:4502 \
  -Dsling.it.instance.runmode=author
```

hoặc Cypress/Playwright point `baseUrl` vào **AEM Publish** (hoặc Dispatcher để test full stack). Chia test theo happy path — UI suite dài thành nhất CI.

## Dispatcher config validation (AMS)

AMS 6.5 dùng **AEM Dispatcher SDK**, không dùng `dispatcher-cloud-optimized` CLI (đó là aaaCS). Chạy validator trên PR nào cũng nên.

`scripts/validate-dispatcher.sh`:

```bash
#!/usr/bin/env bash
set -euo pipefail

DIS_SDK="${DISPATCHER_SDK_HOME:?set DISPATCHER_SDK_HOME to the extracted SDK path}"
"$DIS_SDK/bin/validate.sh" dispatcher/src
```

Dispatcher config hay regression lặng lẽ — thiếu `/allow` trong `filters.any` là lộ `/system/console`. Merge gate bắt buộc.

## Code coverage JaCoCo

`core/pom.xml` (`prepare-agent` + `report`):

```xml
<plugin>
  <groupId>org.jacoco</groupId>
  <artifactId>jacoco-maven-plugin</artifactId>
  <executions>
    <execution><goals><goal>prepare-agent</goal></goals></execution>
    <execution>
      <id>report</id>
      <phase>test</phase>
      <goals><goal>report</goal></goals>
    </execution>
  </executions>
</plugin>
```

Report: `core/target/site/jacoco/index.html`. Coverage ý nghĩa cho Sling Models + services, đừng theo 100% cho HTL/generated code.

## Smoke-test checklist (pre-release)

:::note\[AEM 6.5 / AMS — pre-release QA]
Sau khi deploy lên author + publish AMS:
:::

- Create page với đủ custom component; save + reopen mỗi dialog.
- Publish 1 trang content, verify rendering trên Publish.
- Exercise Dispatcher: request 1 trang 2 lần — lần 2 phải là cache hit (`X-Cache-Info` hoặc `cache/` dir).
- Hit `/system/console/*` qua Dispatcher từ outside — phải trả **404**.
- Verify clientlibs load không 404 (Network tab).
- Chạy `validate.sh` với dispatcher config hiện tại.

## CI pipeline tips (6.5)

- Unit tests mỗi commit — nhanh, đừng block vì infra.
- Integration (`JCR_OAK` query) mỗi PR lên `main`.
- Dispatcher validate make merge gate — 2s, blast radius cao.
- UI tests chống ephemeral author + publish env, không phải mỗi PR. Chạy nightly hoặc merge `main`.
- Publish JaCoCo report làm build artifact.

## Common pitfalls

- Quên `addModelsForClasses` → `adaptTo` trả `null`, fail 3 dòng sau với NPE.
- Sai `ResourceResolverType` → JCR-SQL2 under `RESOURCERESOLVER_MOCK` trả 0 result, test "pass" vì lý do sai.
- `TagManager.resolve()` hoạt động trong `JCR_OAK` nhưng stub dưới `RESOURCERESOLVER_MOCK`. Load tag vào `/content/cq:tags/...` hoặc dùng `JCR_OAK`.
- Test với hardcoded `admin` trong integration — anti-pattern, mirror reality. Dùng service users, test cũng phải chạy được.
- Coupled tới property order — `ValueMap` iteration order không đảm bảo.
- `println` / `LOG.debug` breadcrumbs — noise trong CI log. Dùng AssertJ descriptive failures thay.

## Link ref

- [wcm.io AEM Mocks](https://wcm.io/testing/aem-mock/) — `AemContext` JUnit integration
