Approaches for Testing Sling Applications

  • Mockito — tasty mocking framework for unit tests in Java
  • Sling Mocks — very lightweight, emulates all core features of Sling API and resource resolving. Covers 90% of what is required for typical Sling applications.
  • Integration Tests (Sling Launchpad with Sling Testing Tools) — heavy-weight, for complex scenarios beyond scope of Sling Mocks

Goals for Sling Unit Tests

  • Startup/Teardown of Sling environment for each test run
  • Allow parallel execution of unit tests
  • Support basic OSGi features (via OSGi mocks)
  • Support Sling request, response and resource handling
  • Support Sling Models
  • Support simple loading and creating of content
  • Allow combination with Mockito mocks
  • Visualize code coverage in IDE and CI
  • Maximum execution speed!

Example: Controller with Mocked Business Class

@Model(adaptables = SlingHttpServletRequest.class)
public class MatrixNavLink {

    private MatrixNavigator root;

    @Inject
    public MatrixNavLink(@Self MatrixNavigator navigator) {
        root = navigator.getMatrixNavigation();
    }

    public MatrixNavigator getRoot() {
        return root;
    }
}

Testing with Mockito

@ExtendWith(MockitoExtension.class)
class MatrixNavLinkTest {

    @Mock
    private MatrixNavigator matrixNavigator;

    private MatrixNavLink underTest;

    @BeforeEach
    void setUp() {
        when(matrixNavigator.getMatrixNavigation())
            .thenReturn(new NavigationPageItem("dummy"));
        underTest = new MatrixNavLink(matrixNavigator);
    }

    @Test
    void testGetRoot() {
        assertNotNull(underTest.getRoot());
    }
}

When Mockito Gets Problematic

Pure Mockito becomes difficult when:

  • Iterating over resource or page hierarchies
  • Interaction with objects that have many methods for similar things (e.g. request params, Resource properties)
  • Code that needs a complex content structure as test fixture

Result: 75% mocking code, 25% test code — risk of error in mocking is higher than in production code.

Introducing Sling Mocks

Sling Mocks is a mock implementation of the most important parts of Sling API and resource resolving. Builds on OSGi Mocks, JCR Mocks, and ResourceResolver Mocks.

A SlingContext object gives easy access to all Sling context objects:

  • Registering OSGi services and Sling Models
  • Loading content from JSON files
  • Creating test content with simplified builder API
  • Manipulating request state / investigating response state

Targets everything below the presentation layer — HTL scripts are not tested, but controllers and business logic are.

SlingContext — JUnit 5

@ExtendWith(SlingContextExtension.class)
class MatrixTest {

    private final SlingContext context = new SlingContext();

    @Test
    void testSomething() {
        Resource resource = context.resourceResolver()
            .getResource("/content/matrix/en");
        // further testing
    }
}

// Available context methods:
// context.bundleContext()
// context.resourceResolver()
// context.currentResource()
// context.request()
// context.requestPathInfo()
// context.response()
// context.slingScriptHelper()
// context.runMode("author")

Resource Resolver Types

  • RESOURCERESOLVER_MOCK (default) — fastest, mocked resolver from Sling Testing. No JCR, eventing support, no search.
  • RESOURCEPROVIDER_MOCK — same but using real Sling Resource Resolver impl. Multiple resource providers and FileVault XML loading.
  • JCR_MOCK — mocked JCR with real Sling resource-JCR mapping. Still fast, but mocked search, no Observation/Versioning.
  • JCR_OAK — real Oak with full Node Types, Search, Observation. No fulltext search (no Lucene indexes).

Getting and Manipulating Resources

@Test
void testSomething() {
    Resource resource = context.resourceResolver()
        .getResource("/content/matrix/en");
    ValueMap props = resource.getValueMap();
    Iterator<Resource> children = resource.listChildren();
}

@Test
void testResourceOperations() throws Exception {
    Resource resource = context.resourceResolver().create(
        parentResource, "sentinel",
        Map.of("prop1", "value1", "prop2", 123));
    // further testing
    context.resourceResolver().delete(resource);
}

Simulate Sling Request

// prepare sling request
context.request().setQueryString("param1=aaa&param2=bbb");
context.requestPathInfo().setSelectorString("selector1.selector2");
context.requestPathInfo().setExtension("html");
context.requestPathInfo().setSuffix("/abc.html");

// set current resource
context.currentResource("/content/matrix/en");

// Request Mock supports: Parameters, RequestPathInfo,
// Attributes, Session, Headers, Cookies, Methods, Server Info
// Response Mock supports: Content Type, Headers, Cookies, Response Body

Sling Models in Tests

@BeforeEach
void setUp() {
    // register models from package (includes all subpackages)
    context.addModelsForPackage("org.matrix.models");
}

@Test
void testSomething() {
    SentinelModel model = context.request()
        .adaptTo(SentinelModel.class);
    assertNotNull(model);
}

Loading Content from JSON

@BeforeEach
void setUp() {
    // load JSON file into repository
    context.load().json("/sample-data.json", "/content/matrix/en");

    // load binary file into repository
    context.load().binaryFile("/sample-file.gif",
        "/content/binary/sample-file.gif");
}

@Test
void testSomething() {
    Resource resource = context.resourceResolver()
        .getResource("/content/matrix/en");
    // further testing
}

// Tip: handcraft small JSON snippets per group of test cases.
// JSON download (.tidy.999.json) can be a good start,
// then remove everything not needed.

Building Content with Fluent API

// create resource with properties
Resource resource = context.create().resource("/content/matrix/test1",
    "prop1", "value1",
    "prop2", 123);

// create hierarchies using ResourceBuilder
context.build().resource("/content/matrix/test1")
    .siblingsMode()
    .resource("child1", "prop1", "value1")
    .resource("child2", "prop2", "value2")
    .resource("child3", "prop3", "value3");

// Missing nodes in hierarchy are created automatically
// Properties are given as key,value,key,value... pairs or as Map

Transaction Handling in Tests

Normally you do not have to call resourceResolver.commit() in unit tests. In most cases tests work with one resource resolver and see all changes without commit.

  • context.load() automatically commits after import
  • context.create() currently does not
  • Exception: If using JCR_OAK and JCR-specific features (Query, Observation), you must call commit() first

Sling Hamcrest Matchers

// assert resource properties via hamcrest matcher
assertThat(resource, ResourceMatchers.props(
    "prop1", "value1",
    "prop2", 123));

// Checks that given properties are present with given values,
// although more properties may exist.

// Provided matchers:
// ResourceMatchers: name, properties, children, resource type
// ResourceCollectionMatchers: assert a set of resource paths
// ResourceIteratorMatchers: assert a set of resource paths

Summary

  • There is no "best way" for all cases — use Mockito and/or Sling Mocks whatever suits best
  • Goal: Tests that are easy to write and easy to maintain
  • Tests should focus on the logic of the testable, and only this
  • Goal: 80% unit test coverage for all projects
  • Constantly check test coverage in your IDE
  • Use Infinitest plugin (Eclipse/IntelliJ) to run tests automatically on each change