Wednesday, 15 June 2016

AEM Unit Testing

AEM Unit testing of java code can be a bit of a pain.  Here is an example search servlet and the unit test that goes with it.

Servlet

This is a really simple servlet which searches for resources on a particular path

import java.io.IOException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.logging.Logger;

import javax.management.Query;
import javax.naming.directory.SearchResult;

import org.w3c.dom.Node;

/**
 * Servlet that can be used to search for nodes flagged as current = true on nt:unstructured nodes.
 */
@SlingServlet(paths = "/bin/search/current", methods = "GET")
public class SearchCurrentServlet extends SlingSafeMethodsServlet
{
    /**
     * Default serialisation.
     */
    private static final long serialVersionUID = 1L;

    /**
     * Logger.
     */
    private static final Logger LOG = LoggerFactory.getLogger(SearchCurrentServlet.class);

    /**
     * The path to search.
     */
    private static final String SEARCH_PATH = "/content/my/website/data";

    /**
     * The JSON indentation level.
     */
    private static final int JSON_INDENTATION_LEVEL = 4;

    /**
     * The method called by the get.
     * {@inheritDoc}
     */
    @Override
    protected void doGet(final SlingHttpServletRequest request, final SlingHttpServletResponse response) throws ServletException, IOException
    {
        // Get the properties that have been requested
        final Map<String, Object> map = new HashMap<>();
        map.put("type", "nt:unstructured");
        map.put("path", SEARCH_PATH);

        map.put("1_property", "current");
        map.put("1_property.value", true);

        // Get the QueryBuilder and do the query from the Map above.
        final Session session = request.getResourceResolver().adaptTo(Session.class);
        final QueryBuilder builder = request.getResourceResolver().adaptTo(QueryBuilder.class);
        final Query query = builder.createQuery(PredicateGroup.create(map), session);
        query.setHitsPerPage(Integer.MAX_VALUE);
        final SearchResult result = query.getResult();

        // Iterate over the results
        final Iterator<Resource> resources = result.getResources();
        try
        {
            while (resources.hasNext())
            {
                // Get the next resource, convert to Node and make it as JSON.
                final Resource resource = resources.next();
                final Node node = resource.adaptTo(Node.class);
                final JsonJcrNode jsonJcrNode = new JsonJcrNode(node, getNodeIgnoreSet());
                response.getOutputStream().println(jsonJcrNode.toString(JSON_INDENTATION_LEVEL));
            }
        }
        catch (final RepositoryException | JSONException e)
        {
            response.getOutputStream().println("Error iterating over the results " + e.getMessage());
            LOG.error("Error iterating over the search results", e);
        }

        response.flushBuffer();
    }
}


Unit Test

The unit test uses AemContext to set up a load of mocks which are needed for the test.  However, the AemContext does not give a QueryBuilder object out of the box so an adapter has to be defined for this.  It is added to the AemContext.

import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import javax.jcr.Session;
import javax.servlet.ServletException;

import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.testing.mock.sling.ResourceResolverType;
import org.apache.sling.testing.mock.sling.servlet.MockSlingHttpServletRequest;
import org.apache.sling.testing.mock.sling.servlet.MockSlingHttpServletResponse;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;

import com.day.cq.search.PredicateGroup;
import com.day.cq.search.Query;
import com.day.cq.search.QueryBuilder;
import com.day.cq.search.result.SearchResult;

import io.wcm.testing.mock.aem.junit.AemContext;

/**
 * Test class for the {@link SearchCurrentServlet}.
 */
@RunWith(MockitoJUnitRunner.class)
public class SearchCurrentServletTest
{
    /**
     * The mocked query builder.
     */
    @Mock
    private QueryBuilder queryBuilder;

    /**
     * The AEM context to test with.
     */
    @Rule
    public final AemContext context = new AemContext(aemContext ->
    {
        aemContext.load().json("/result.json", "/result.json");
        aemContext.registerAdapter(ResourceResolver.class, QueryBuilder.class, queryBuilder);
    }, ResourceResolverType.JCR_MOCK);

    /**
     * Test the output in json includes fields from the node.
     *
     * @throws IOException thrown by the servlet method
     * @throws ServletException thrown by the servlet method
     */
    @Test
    public void testDoGet() throws ServletException, IOException
    {
        // Arrange
        final SearchCurrentServlet servlet = new SearchCurrentServlet();
        final MockSlingHttpServletRequest request = context.request();
        final MockSlingHttpServletResponse response = context.response();

        final Query query = mock(Query.class);
        final SearchResult searchResult = mock(SearchResult.class);
        final Resource resource = context.currentResource("/result.json");
        final List<Resource> results = new ArrayList<>();
        results.add(resource);

        when(queryBuilder.createQuery(any(PredicateGroup.class), any(Session.class))).thenReturn(query);
        when(query.getResult()).thenReturn(searchResult);
        when(searchResult.getResources()).thenReturn(results.iterator());

        // Act
        servlet.doGet(request, response);

        // Assert
        // System.out.println(response.getOutputAsString());
        assertTrue(response.getOutputAsString().contains("\"name\": \"Bob\""));
        assertTrue(response.getOutputAsString().contains("\"cost\": \"12.34\""));
    }
}


Unit Test Data

The AemContext loads a json file from src/test/resources (the classpath) and makes it available for the path specified.  When the resource is loaded with context.currentResource("/result.json"); the Resource object returned is made up of the values in the result.json file.

result.json has only the following lines

{
"jcr:primaryType":"nt:unstructured",
        "current":true,
        "name":"Bob",
        "cost":12.34
}



AEM Multifield for Touch UI Dialogs

Multifields are used to group at field set together within AEM.  In a dialog they can be used to store a variable sized array or create a node hierarchy.

Single Field

For a single field the set up is very simple.  The field that can be added and removed is just put straight under the multifield value,


  <description-lines
    jcr:primaryType="nt:unstructured"
    sling:resourceType="granite/ui/components/foundation/form/multifield"
    fieldLabel="Description Lines"
    renderReadOnly="{Boolean}true">
    <field
      jcr:primaryType="nt:unstructured"
      sling:resourceType="granite/ui/components/foundation/form/textfield"
      name="./descriptionLines"/>
  </term-description>

This multifield will store an array of the values entered into the ./descriptionLines value in the JCR.

Field Set

For a genuine set of fields that need to be grouped together the hierarchy under the multifield definition is more complicated,

  <person
    jcr:primaryType="nt:unstructured"
    sling:resourceType="/apps/touch-ui-multi-field-panel/multifield"
    class="full-width"
    fieldDescription="Click '+' to add a new person"
    fieldLabel="People">
    <field
      jcr:primaryType="nt:unstructured"
      sling:resourceType="granite/ui/components/foundation/form/fieldset"
      name="./items">
      <layout
        jcr:primaryType="nt:unstructured"
        sling:resourceType="granite/ui/components/foundation/layouts/fixedcolumns"
        method="absolute"/>
      <items jcr:primaryType="nt:unstructured">
        <column
          jcr:primaryType="nt:unstructured"
          sling:resourceType="granite/ui/components/foundation/container">
          <items jcr:primaryType="nt:unstructured">
            <name
              jcr:primaryType="nt:unstructured"
              sling:resourceType="granite/ui/components/foundation/form/textfield"
              fieldDescription="Enter Name"
              fieldLabel="Name"
              name="./name"/>
            <age
              jcr:primaryType="nt:unstructured"
              sling:resourceType="granite/ui/components/foundation/form/textfield"
              fieldDescription="Enter Age"
              fieldLabel="Age"
              name="./age"/>
          </items>
        </column>
      </items>
    </field>
  </person>


This multifield creates a subnode under ./items each of which contains a 'name' and 'age' field as defined as textfields in the set up.