Friday 25 March 2016

In-container testing for AEM projects

Nowadays it’s easier than ever to encapsulate the state used by AEM components into objects – commonly referred to as models – which can then be used while rendering the response. For example, the Sling Modelsframework is a great way to do this. If the Sightly format is used for your template, you can only use very simple presentation logic, meaning you must use a model class to do more complex operations.

One of the many benefits of this approach is that your model classes can be automatically tested as part of a continuous integration (CI) set-up. When using Java models, a common approach is to use unit tests to do this, and rely on 
Mockito or similar frameworks to simulate the behaviour of the AEM environment. Within such a rich environment, however, the test code quickly becomes hard to follow, as most of it is setting up the AEM simulation. Worse still, it’s very easy to get the simulation wrong, meaning your tests pass but your code is actually buggy.



These problems are magnified when writing other types of AEM code. For example, a feed importer that imports product data into the JCR becomes very cumbersome to test in this manner. Also, consider if you create an OSGi service using SCR annotations - it’s possible your code is correct but the annotation is wrong, meaning your service may never be made available within AEM. Your unit tests would pass but your code would never work when deployed.
In-container testing for AEM projects
This article will look at running integration tests via HTTP against a running AEM instance or container. For a continuous integration set-up, the AEM instance is created, started and shut down as part of the Maven build cycle. For local development, the same tests can be run against an already running AEM instance to speed up the test process.
Maven will be used as the build tool, as this is the usual standard in AEM projects. However, although some Maven plugins are used for build set-up, this approach can be used with any build tool.
For the purpose of this article I’ll be using the terms unit test and integration test rather loosely. By unit test I mean a test that can be set up and run very quickly (no more than half a second or so) outside of any container. By integration test I mean any test that is run within an AEM instance.
Sling testing tools
The good news is that the Apache Sling project supplies the Sling Testing Tools module, which provides several ways to run tests in a Sling environment. As AEM has Sling at its core, we can use these tools to test our code.
The SlingTestBase class can be used as a superclass for tests to be run against a running AEM instance. By default, the first time this class is used it will try to locate the Sling runnable jar and start it. The AEM quickstart.jarcan be run in the same manner.
Setting up and running the example
You will need a valid AEM licence and the AEM quickstart.jar file checked into a Maven repository. The quickstart.jar file should contain the following Maven co-ordinates:
<dependency>
    <groupId>com.adobe.aem</groupId>
    <artifactId>cq-quickstart</artifactId>
    <version>6.0.0</version>
    <classifier>standalone</classifier>
</dependency>
If you don’t have a Maven repository readily available, there are several ways to set one up. A very convenient way is to use Amazon S3 as a Maven repository: Bruce Li has created a working example. Then follow this guide to deploy the quickstart jar file.
Check out the example project and edit the license.properties file. Put your valid licence details in there.
You also need to configure Maven to use the repository you installed the quickstart into. One way to do this is to add into the top-level pom. Find the section with the adobe-public repository and duplicate, omitting the<pluginRepository\> section and rename/configure as appropriate. Read the README file for further setup details.
To run the example, execute the following from the root of the project
mvn clean verify -P integrationTests
You should see Maven build the project, start up a new AEM instance, deploy the project and run the tests against this.
The AEM start-up sequence
The integration tests are run from the it.launcher pom, when theintegrationsTests Maven profile is specified. The steps are as follows:
1.     The AEM quickstart.jar file is retrieved as a Maven dependency and copied to /target/dependency, along with other bundles that will need to be deployed to the new instance.
2.  <plugin>
3.      <groupId>org.apache.maven.plugins</groupId>
4.      <artifactId>maven-dependency-plugin</artifactId>
5.      <version>2.8</version>
6.      <executions>
7.          <execution>
8.              <id>copy-runnable-jar</id>
9.              <goals>
10.                  <goal>copy-dependencies</goal>
11.              </goals>
12.              <phase>process-resources</phase>
13.              <configuration>
14.                  <includeArtifactIds>cq-quickstart</includeArtifactIds>
15.                  <excludeTransitive>true</excludeTransitive>
16.                  <overWriteReleases>false</overWriteReleases>
17.                  <overWriteSnapshots>false</overWriteSnapshots>
18.              </configuration>
19.          </execution>
20.          <execution>
21.              <!--
22.              Consider all dependencies as candidates to be installed
23.              as additional bundles. We use system properties to define
24.              which bundles to install in which order.
25.              -->
26.              <id>copy-additional-bundles</id>
27.              <goals>
28.                  <goal>copy-dependencies</goal>
29.              </goals>
30.              <phase>process-resources</phase>
31.              <configuration>
32.                  <outputDirectory>${project.build.directory}/sling/additional-bundles</outputDirectory>
33.                  <excludeTransitive>true</excludeTransitive>
34.                  <overWriteReleases>false</overWriteReleases>
35.                  <overWriteSnapshots>false</overWriteSnapshots>
36.              </configuration>
37.          </execution>
38.      </executions>
 </plugin>
39.   The AEM license.properties file is copied into the right place.
40.  <plugin>
41.     <groupId>org.apache.maven.plugins</groupId>
42.     <artifactId>maven-antrun-plugin</artifactId>
43.     <executions>
44.         <execution>
45.             <id>copy-aem-license</id>
46.             <phase>process-resources</phase>
47.             <configuration>
48.                 <tasks>
49.                     <mkdir dir="${jar.executor.work.folder}"/>
50.                     <copy file="${project.basedir}/src/test/resources/license.properties"
51.                           toDir="${jar.executor.work.folder}" verbose="true"/>
52.                 </tasks>
53.             </configuration>
54.             <goals>
55.                 <goal>run</goal>
56.             </goals>
57.         </execution>
58.  
59.     </executions>
 </plugin>
60.   A random port is reserved for the AEM server.
61.  <plugin>
62.      <!-- Find free ports to run our server -->
63.      <groupId>org.codehaus.mojo</groupId>
64.      <artifactId>build-helper-maven-plugin</artifactId>
65.      <version>1.9.1</version>
66.      <executions>
67.          <execution>
68.              <id>reserve-server-port</id>
69.              <goals>
70.                  <goal>reserve-network-port</goal>
71.              </goals>
72.              <phase>process-resources</phase>
73.              <configuration>
74.                  <portNames>
75.                      <portName>http.port</portName>
76.                  </portNames>
77.              </configuration>
78.          </execution>
79.      </executions>
 </plugin>
80.   The JaCoCo plugin is configured to record our test coverage.
81.  <plugin>
82.      <groupId>org.jacoco</groupId>
83.      <artifactId>jacoco-maven-plugin</artifactId>
84.      <version>0.7.2.201409121644</version>
85.      <configuration>
86.          <append>true</append>
87.      </configuration>
88.      <executions>
89.          <execution>
90.              <goals>
91.                  <goal>prepare-agent-integration</goal>
92.              </goals>
93.              <configuration>
94.                  <dumpOnExit>true</dumpOnExit>
95.                  <output>file</output>
96.                  <includes>
97.                      <include>com.ninedemons.*</include>
98.                  </includes>
99.                  <append>true</append>
100.                           <propertyName>jacoco.agent.it.arg</propertyName>
101.                       </configuration>
102.                   </execution>
103.               </executions
 </plugin>
104.The Maven failsafe plugin is then configured with various settings needed as system properties, and starts running the integration tests.
The quickstart.jar is configured to not start a browser, run in author mode and to not install the sample content to reduce start-up time:
 <!-- Options for the jar to execute. $JAREXEC_SERVER_PORT$ is replaced by the selected port number -->
 <jar.executor.jar.options>-p $JAREXEC_SERVER_PORT$ -nobrowser -nofork -r author,nosamplecontent</jar.executor.jar.options>
105.The very first test to run will start the AEM instance: wait for it to start (by polling the URL set in the <server.ready.path.1> property) and then install additional bundles.
The additional bundles installed are the Sling Testing bundles, our project bundles and the httpclient-osgi and httpcore-osgi dependencies needed by Sling Testing:
 <!--
     Define additional bundles to install by specifying the beginning of their artifact name.
     The bundles are installed in lexical order of these property names.
     All bundles must be listed as dependencies in this pom, or they won’t be installed.
 -->
 <sling.additional.bundle.1>org.apache.sling.junit.core</sling.additional.bundle.1>
 <sling.additional.bundle.2>org.apache.sling.junit.scriptable</sling.additional.bundle.2>
 <sling.additional.bundle.3>example.models</sling.additional.bundle.3>
 <sling.additional.bundle.5>example.core</sling.additional.bundle.5>
 <sling.additional.bundle.6>org.apache.sling.junit.remote</sling.additional.bundle.6>
 <sling.additional.bundle.7>org.apache.sling.testing.tools</sling.additional.bundle.7>
 <sling.additional.bundle.8>httpclient-osgi</sling.additional.bundle.8>
 <sling.additional.bundle.9>httpcore-osgi</sling.additional.bundle.9>
106.From this point onwards, the tests can use HTTP to set up and run tests within AEM.
107.When all tests are finished, the running AEM instance is shut down by default. This means that JaCoCo test coverage is finalised in a file atit.launcher/target/jacoco-it.exec and can be used together with tools such as SonarQube to report test coverage.
How the tests are written
In our example we have one Java model beancom.ninedemons.aemtesting.models.title.TitleModel. This model is used in the title component Sightly file title.html.
We have a test class for this model: TitleModelTest.java This class extendsSlingTestBase, which in turn takes care of starting up the AEM instance, if needed. It makes use of all the standard JUnit annotations to mark tests and set up code.
The first thing that the test does is to create an instance of SlingClient.
private SlingClient slingClient = new SlingClient(this.getServerBaseUrl(), this.getServerUsername(), this.getServerPassword());
This now allows the test to speak to the AEM instance using the Sling RESTful API.
Setting up a test component
The first place the SlingClient is used is to set up a test component. There’s a minimal JSP used for testing the model – remember, we’re testing the model not the Sightly markup: titleModelTest.jsp. This JSP simply creates an instance of the model and renders it as pretty plain HTML:
<sling:adaptTo adaptable="${slingRequest}" adaptTo="com.ninedemons.aemtesting.models.title.TitleModel" var="model"/>

Element is '${model.element}' <br/>
Title is '${model.text}' <br/>
Our test creates an apps folder in the AEM instance and uploads the JSP:
private void uploadTestJsp() throws IOException {

    slingClient.upload(
            TEST_APP_FOLDER_PATH + "/title-model-test.jsp",
            TitleModelTest.class.getClassLoader().getResourceAsStream(
                    "jsp/title-model/titleModelTest.jsp"), -1, true);
}
Once this is done, we can use this test component by setting thesling:resourceType of a JCR node to test/title-model-test.
Creating test pages
Now the test creates a test page and JCR nodes to reference the test component:
private void createTestPageComponent() throws IOException {

    slingClient.createNode(PATH_TO_TEST_NODE,
            JcrConstants.JCR_PRIMARYTYPE,JcrConstants.NT_UNSTRUCTURED,
            JcrResourceConstants.SLING_RESOURCE_TYPE_PROPERTY, "test/title-model-test",
            "type", TITLE_TYPE,
            JcrConstants.JCR_TITLE,EXPECTED_TITLE,
            JcrConstants.JCR_DESCRIPTION,"Test Node For SimpleModel"
    );
}
Running a test scenario
Now the test component and pages are in place, a test can be run against the AEM instance:
@Test
public void whenAllPropertiesSetAgainstComponent() throws Exception {

    // Given a component instance where the title and type are set against
    // the instance node

    // When the component is rendered
    RequestExecutor result = getRequestExecutor().execute(
            getRequestBuilder().buildGetRequest(
                    PATH_TO_TEST_NODE + ".html").withCredentials(
                    this.getServerUsername(), this.getServerPassword()));

    // Then the model should return the values defined in the instance node
    result.assertStatus(200)
            .assertContentContains("Element is \'" + EXPECTED_ELEMENT + "\'")
            .assertContentContains("Title is \'" + EXPECTED_TITLE + "\'");

}
This test requests that the test component be rendered as HTML, meaning our test JSP is used. The test checks the response is a HTTP OK code (200), and that the expected HTML element and title text is used.
Using an already running AEM instance
Waiting for the AEM instance to start up each time you want to test during development is impractical. On a powerful MacBook Pro Retina with 16Gb of memory and an SSD it takes around four minutes to run a test cycle. Instead, it’s much better to use an already running AEM instance to run the tests against. Thankfully, this is very easy by using a property calledtest.server.url passed to Maven:
mvn clean verify  -P integrationTests -Dtest.server.url=http://localhost:4502
In this mode, the SlingTestBase class simply checks that the URL is accessible and skips starting up the instance. However, the additional bundles are installed as per the pom.
The AEM instance is not terminated after the tests finish, allowing any investigation needed for failing tests. It also means debugging by attaching an IDE is possible.
Other test modes
The Sling Testing Tools project allows for other ways of running tests – for example, running unit tests within the AEM instance itself. It’s well worth reading up on the various tools that the project supplies.
Conclusion
This approach to testing has several benefits above using unit tests and mocking AEM behaviour. The test code is easier to read and maintain, and gives a high level of confidence that your code is correct as it’s running in a real AEM instance. It’s also very easy to run your tests against a new version of AEM – simply update the AEM dependency in your pom to reference the new version.
There are, however, some disadvantages – the most obvious being the overhead involved in the AEM start-up. As described above, on a powerful workstation this can be four minutes, and on a typical build server it’s usually around 10 minutes.
A more serious limitation is around service packs or similar updates to AEM. It would be possible to install these in a similar manner to additional bundles, but some service packs have needed human intervention to restart the instance at the necessary points. This, of course, is not possible from our tests.
You must also make the quickstart.jar file available in a Maven repository somewhere, and embed your AEM licence details in your source tree.
These limitations can be addressed by using an already running AEM instance with all the updates applied. The problem here is ensuring the instance is clean before the tests start and resetting after the tests finish, and ensuring only one build is testing against this AEM instance at any one time.
A very effective and powerful method to solve these issues is to use Dockerto provision new AEM instances quickly for every build.
The source code referred to in this article is available onGitHub.


No comments :

Post a Comment