Testing an AEM 6.5 LTS Project
Test pyramid AEM 6.5
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.
[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:
Chọn ResourceResolverType
AemContext ctx = new AemContext(ResourceResolverType.JCR_MOCK);
[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.
Link ref