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, and this page is available as Markdown at https://nhanbe.id.vn/others/posts/testing-aem-project.md.

AemContext, Sling Model / OSGi service unit tests, JCR_OAK queries, UI tests, Dispatcher validation for AEM 6.5 LTS.

Testing an AEM 6.5 LTS Project

Test pyramid AEM 6.5

LayerScopeRuntime
UnitSling Models, services, utilities + mocked repository~100 ms/class
Integration-stylerepository + OSGi wiring, in-memory JCR~1 s/class
UIauthoring 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)

PurposeToolNote
RunnerJUnit 5 (junit-jupiter)@ExtendWith(AemContextExtension.class)
AEM + Sling mockswcm.io AEM Mocks io.wcm.testing.mock.aem-junit5De facto standard, bundle Sling Mocks + OSGi Mock + AEM extension
AssertAssertJ hoặc plain JUnitAssertJ chaining đọc object sâu hay hơn
Mock collaboratorMockitoChỉ cho external; Sling/OSGi service ưu tiên AemContext.registerService
HTTP stubWireMockStub external API
UI testsSelenium via ui.tests module, hoặc Cypress/PlaywrightHobbes.js deprecated
Dispatcher validationAMS Dispatcher SDK validate.shAMS 6.5, không phải dispatcher-cloud-optimized CLI
[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

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:

APIChứ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

AemContext ctx = new AemContext(ResourceResolverType.JCR_MOCK);
TypeBackendJCR queryDùng khi
RESOURCERESOLVER_MOCK (default)in-memory map❌95% Sling Model tests, nhanh nhất (ms)
JCR_MOCKfake JCRlimitedCần Node.getProperty() nhưng không query
JCR_OAKOak segment store in-memory✅ JCR-SQL2, XPathDùng QueryBuilder / SQL2
JCR_JACKRABBITJackrabbit 2 thật✅Legacy, hiếm
[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

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

src/test/resources/content/teaser.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

// /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

@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)

@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)

@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.

@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));
}
[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

@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

@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

@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

@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.

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:

#!/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):

<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)

[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.