Automating AEM Component Unit Tests with JUnit and Mockito
AEM components often look simple in the browser while hiding a network of Sling Models, OSGi services, resource properties, inherited configuration and conditional rendering. A small change to a dialog field can therefore affect both the generated markup and the business logic behind it. Unit testing gives Java and AEM teams a fast way to detect those regressions before they reach an authoring environment or a production release.
JUnit provides the test structure, assertions and lifecycle management. Mockito supplies controlled substitutes for services, requests, resources and external dependencies. Together, they allow developers to test component behaviour without starting a full AEM instance for every build.
This approach is especially useful for Australian delivery teams working across Sydney, Melbourne, Brisbane and Perth. Distributed developers can receive feedback from a continuous integration server within minutes instead of waiting for a shared authoring environment. Tests also make local requirements, such as en-AU formatting, daylight-saving behaviour and Australian content variations, explicit in the codebase.
A reliable test suite does not replace integration or browser testing. It creates a fast first layer that validates component decisions in isolation, leaving slower environments to verify repository wiring, permissions, client libraries and end-to-end authoring workflows.
Why Unit Testing Matters in AEM Projects
AEM component logic commonly sits in Sling Models, Java Use-API classes, OSGi services and helper classes. When that logic is tested only through a deployed page, failures are expensive to diagnose. A failing unit test can identify the exact method, input or mocked dependency responsible for the problem.
Unit tests are particularly valuable for reusable components such as navigation, cards, teasers, search results and personalised offers. They can verify that a missing image produces a defined fallback, that an empty multifield does not cause a null pointer exception, or that a service response is mapped into the expected view model.
For an Australian retail or financial services site, tests can cover details that are easy to overlook during generic development. A price can be checked for Australian currency formatting, a date can be validated against local conventions, and content can be tested for the correct Sydney or Melbourne campaign variant. These examples turn localisation from an informal review step into an executable requirement.
Setting Up JUnit and Mockito
Most modern AEM Maven projects use JUnit 5, although older codebases may still depend on JUnit 4. The choice should match the project’s AEM SDK, testing libraries and build plugins. A typical test module includes JUnit Jupiter, Mockito Core, Mockito JUnit Jupiter and an AEM mocking framework such as wcm.io AEM Mocks.
AEM Mocks are useful when a test needs realistic Resource, Page, Asset or SlingHttpServletRequest behaviour. Mockito is better suited to narrow collaborations, such as an OSGi service that returns customer data. Keeping those roles separate prevents tests from becoming an imitation of the entire repository.
A basic test class can use the Mockito extension and inject a mock dependency:
@ExtendWith(MockitoExtension.class)
class PromoModelTest {
@Mock
private OfferService offerService;
@InjectMocks
private PromoModel promoModel;
@Test
void returnsOfferTitle() {
when(offerService.findOffer("summer"))
.thenReturn(new Offer("Summer savings"));
promoModel.setCampaignId("summer");
assertEquals("Summer savings", promoModel.getTitle());
}
}
The example is deliberately small. A test should establish the input, configure relevant collaborators, invoke one behaviour and assert an observable result. Avoid asserting private fields or implementation details that can change during refactoring.
Building a Useful AEM Test Fixture
A good fixture represents the minimum content and context needed by the component. With AEM Mocks, a test can create an in-memory resource tree, register services and adapt a resource to a Sling Model. This provides realistic adaptation behaviour without requiring a running publish or author instance.
@ExtendWith(AemContextExtension.class)
class CardModelTest {
private final AemContext context = new AemContext();
@Test
void readsCardProperties() {
context.create().resource("/content/site/card",
"jcr:title", "Visit Sydney",
"sling:resourceType", "site/components/card");
CardModel model = context.resourceResolver()
.getResource("/content/site/card")
.adaptTo(CardModel.class);
assertEquals("Visit Sydney", model.getTitle());
}
}
Fixtures should include realistic property names, resource types and child nodes. They should also cover absent values, empty strings, malformed data and optional services. A model that works only when every author-entered property is present is not robust enough for production.
Use @BeforeEach to reset state and avoid sharing mutable objects between tests. Name tests around behaviour, such as returnsDefaultImageWhenAssetIsMissing, rather than internal methods. Clear names help developers in Canberra, Adelaide or remote locations understand a failure without opening the implementation first.
Mocking OSGi Services and External Integrations
Mockito becomes essential when a component depends on an OSGi service, HTTP client, analytics provider or personalisation engine. The test should control the dependency’s response and then verify how the component reacts. It should not make live Salesforce calls, access a remote API or depend on a particular network condition.
For a deeper example of the sort of integration that may sit behind a personalised component, see Salesforce personalisation. The unit test for that component could mock the profile service, return a known segment and assert that the correct offer or content path is selected.
Mockito verification is useful when collaboration itself is part of the behaviour:
when(profileService.segmentFor("customer-42"))
.thenReturn("frequent-traveller");
String path = resolver.resolve("customer-42");
assertEquals("/content/site/offers/travel", path);
verify(profileService).segmentFor("customer-42");
Do not overuse interaction assertions. Verifying every call can make a test brittle when the implementation is simplified. Prefer checking the returned model, rendered value or selected path, then verify a dependency only when an unwanted call would create a meaningful defect, such as duplicate billing, repeated API traffic or a privacy-sensitive lookup.
Testing HTL, Sling Models and Metadata Logic
HTL templates are primarily presentation, so their Java unit tests should focus on the Sling Model or service that supplies the data. Test visibility rules, link construction, image selection, accessibility labels and fallback text. Integration tests can later confirm that HTL binds those values correctly and that the final HTML contains the expected attributes.
When a component processes images, PDFs or office documents, metadata extraction can introduce another boundary. A test should provide a controlled metadata result rather than parse a large binary file each time. Teams working with AEM Assets may find the discussion of rich media metadata useful when deciding which extraction concerns belong in unit tests and which belong in integration coverage.
For example, a metadata service test can return a MIME type, title and dimensions, then assert that the model exposes an appropriate label. Additional cases should cover an unsupported format, missing EXIF data and an extraction exception. These tests are valuable for Australian publishers and retailers handling supplier imagery from different systems, where metadata quality can vary significantly.
Keep parsing, validation and presentation decisions in separate classes where possible. A small metadata mapper is easier to test than a Sling Model that reads repository content, invokes Apache Tika, applies business rules and formats the final label in one method.
Running Tests in CI and Maintaining Coverage
Tests should run automatically with Maven on every pull request and branch build. A typical pipeline compiles the project, executes unit tests, generates a JaCoCo report and then proceeds to AEM integration or UI tests. Fast tests should remain independent of local author instances so that a developer in Melbourne and a build agent in Sydney receive consistent results.
Coverage percentages are useful signals, but they are not a quality guarantee. A suite can achieve high line coverage without checking meaningful edge cases. Measure whether important branches are exercised: missing content, service failures, invalid author input, permissions, locale differences and empty search results.
The AEM conference community also demonstrates the value of sharing repeatable technical practices. A team can use the event app to revisit session material and coordinate learning across developers, architects and systems engineers, then translate those ideas into project standards for test naming, fixture management and pipeline gates.
Review tests during code review as carefully as production code. Remove duplicated setup, keep mock data understandable and refactor fixtures when component contracts change. For an Australian organisation operating across AEST and AEDT, include time-zone tests that use an explicit ZoneId rather than the build server’s default clock. This prevents seasonal failures that appear only around daylight-saving transitions.
Build a small test suite around one AEM component, then expand it to its dependent services and edge cases. Add JUnit and Mockito to the Maven build, use AEM Mocks only where repository context is necessary, and run the suite in CI on every change. Consistent unit testing will give your team faster releases, safer refactoring and greater confidence in the components supporting Australia’s digital experiences.