Writing Integration Tests for an AEM Maven Project

Integration tests are the unloved middle child of an AEM codebase, sitting between fast unit tests and slow manual UAT. They exercise the full bundle-to-repository path, including Sling resource resolution, the Sightly scripting engine, and JCR persistence. Without them, a Sightly change can quietly break a contact form on a Wednesday afternoon and only surface when a Sydney-based marketing coordinator files a ticket.

For Australian teams delivering on tight SLAs for the big four banks, telcos, or federal departments, the cost of late discovery is more than embarrassment. A bug that reaches a Brisbane production site at lunchtime AEST can mean a 14-hour wait before US-based support sees the alarm. Building reliable integration coverage locally turns that risk into a noisy build failure, which is a much friendlier problem.

The AEM Maven archetype already scaffolds a separate it.tests module for this purpose, but the defaults are a sketch rather than a finished product. The bundle includes Sling Mocks and a sample test class, yet it leaves test content sourcing, profile activation, and CI wiring to the developer. Spending a day refining those early choices pays back every quarter, particularly when the suite starts running on Bamboo or GitHub Actions.

The approach below treats test content as a first-class artefact, versioned in Git. It assumes a recent AEM SDK on Java 11 or 17, Maven 3.8 or newer, and identical behaviour on a developer's MacBook in Melbourne and a shared build agent in Singapore.

Laying Out the Maven Test Module

The default archetype places integration code in it.tests/src/test/java and pulls in sling-mocks, aem-mocks, and junit. That layout works for a proof of concept, but production teams usually want a clearer split between unit-style mocks, content-driven tests, and end-to-end smoke tests. A common refactor introduces three source roots: unit, content, and e2e, each with its own Maven profile so contributors can run subsets with ./mvnw verify -Pcontent.

Inside the content root, a resources/ folder holds the JSON or XML fixtures that imitate real AEM nodes. The classpath should include both compiled classes and test resources, otherwise resource resolution will silently return nulls and assertions will look green while hiding a broken script. A <build> block with a dedicated <resources> section forces Maven to overlay fixtures on the compiled classes.

The maven-failsafe-plugin should run integration tests in the verify phase, keeping mvn test fast for inner-loop work. The <includes> pattern should match **/*IT.java, separating fast unit tests from the slower suite cleanly.

Authoring Test Content as JSON

AEM's content model is hierarchical, which makes JSON a natural fit for fixtures. A typical test page under /content/test can be a single test-page.json that mirrors what the CRXDE import wizard produces. JSON diffs stay readable across Git branches, which matters when test content evolves alongside a Sightly refactor in the same commit.

There are two reasonable strategies for loading JSON into the mock context. The first uses Sling Mocks' AemContext builder, where builder.load().json("/path/to/fixture.json", "/content/test") mounts the content tree directly. The second uses JsonImporter from the aem-mocks package, closer to a real CRX import. Most Melbourne and Brisbane teams prefer the first approach because test setup stays in one fluent call.

For component-level tests, one fixture per component pays off. A teaser.json, text.json, and button.json in /src/test/resources/components keep each test class focused on one concern. Real-world content fixtures from the Science Olympiad archive follow a similar single-record-per-file pattern that translates well into AEM fixtures.

A subtle gotcha is namespaces. AEM persists cq: and sling: prefixes that the standard JSON spec does not cover, so fixtures must quote them or Sling Mocks will silently strip them. A small lint script in CI catches unquoted namespace keys before the test phase ever runs.

Picking a JUnit Generation

The choice between JUnit 4, JUnit 5, and TestNG sounds academic until you hit parallel execution. AEM integration tests are slow because of resource loading, and a 40-minute suite is unacceptable on a CI server that costs real money per minute. JUnit 5 with @TestInstance(Lifecycle.PER_CLASS) and Jupiter extensions handles parallelism more gracefully than JUnit 4, but aem-mocks still ships its base class against JUnit 4 semantics, which means adapters live in your codebase.

TestNG is worth considering if your team already uses it for Selenium. Its data providers map neatly onto test content variants and its reporting is more configurable. The downside is fewer AEM sample projects use TestNG, raising the cost of onboarding new starters in places like Adelaide or Hobart where the local AEM community is small.

Aspect JUnit 4 JUnit 5 TestNG
AEM sample availability High Growing Low
Parallel execution Basic via Surefire First-class via Jupiter Strong, configurable
Adapter needed for aem-mocks None Yes None
Onboarding cost outside Sydney or Melbourne Low Medium Medium-High
Best fit Legacy codebases New AEM projects Teams with existing Selenium grids

For most Australian teams, JUnit 5 is the pragmatic default: modern syntax, parametric tests, and a growing set of AEM samples. Reserve TestNG only if your Selenium grid already depends on it, and avoid JUnit 4 unless you are inheriting an older codebase.

Mock Contexts vs Real AEM Instances

Mock contexts are fast, deterministic, and friendly to a developer's laptop. They shine for component-level tests where you want to assert that a Sightly template renders the correct markup given a fixture tree. The downsides appear when you cross into OSGi services, workflows, or replication, none of which Sling Mocks replicates faithfully. A test that exercises WorkflowSession will pass against mocks but fail against a real author.

Real author instances give you genuine integration coverage but cost more in build minutes. They need careful cleanup so test runs do not leave content that pollutes the next developer's session. A common pattern namespaces test content under /content/test-<timestamp> and deletes it in an @AfterAll hook.

A hybrid approach often wins. Component tests run against Sling Mocks, while a smaller smoke suite runs nightly against a real author instance. The nightly suite catches OSGi wiring problems that mocks ignore. When your pipeline runs on Azure DevOps, layering in policy controls so only approved AEM runtimes host test artefacts helps, as the Azure Policy walkthrough outlines.

OAuth and External APIs in Integration Tests

Integration tests that reach outside AEM deserve special care. A Sightly component that calls a third-party API over OAuth will work in production but fail in CI if the test never acquires a token. Mocking the HTTP layer with WireMock keeps tests fast and avoids leaking real credentials into the build log, but it requires discipline to keep the mock contract aligned with the real service.

For tests that need a live OAuth handshake, a token-caching client is essential. The OAuth 2.0 walkthrough covers sharing a refresh token across the test JVM and reusing it across test classes, which cuts hours off a nightly run.

Test secret storage comes up quickly on Australian engagements touching APRA-regulated data. Inject credentials from a CI secret store, fall back to an environment variable on a developer's machine, and fail loudly when neither is present. A missing credential beats a silently empty token that returns 401s.

CI Pipelines and the Australian Build Reality

Pipelines running in Australian data centres tend to be smaller and slower than their US counterparts, so test parallelism matters more than ever. Splitting the integration suite across multiple Maven profiles and running them on parallel agents in Bamboo or GitHub Actions can cut wall-clock time from 40 minutes to under 12. The trick is keeping each profile self-contained, otherwise shared state will introduce flaky ordering bugs.

Caching ~/.m2/repository between builds is the biggest win on Australian agents, where egress costs add up quickly. Pre-warming the Sling Mocks fixture tree as a JAR artefact is a close second, so each test run does not re-parse JSON on cold start.

Keep an eye on flaky-test metrics. A suite that fails 5% of the time on main erodes trust quickly, and Australian teams have less margin to re-run broken builds than their US peers. Quarantining flaky tests into a nightly profile preserves signal-to-noise without losing coverage.

The CIRCUIT recordings from Chicago cover these patterns end-to-end, with talks from Adobe engineers and practitioners who have shipped AEM sites at scale. If your team needs a refresher on Sightly or a hands-on Sling Mocks walk-through, those recordings are a fair dinkum resource. Download the CIRCUIT app to stream the 2015 and 2016 sessions on demand, and bring the conference back to your team in Sydney, Melbourne, or wherever the next integration bug is waiting.