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