Creating Integration Tests in Apache Sling

By Daniel Klco Daniel Klco on 
This was also posted on: http://labs.sixdimensions.com/blog/dan-klco/2013-06-05/creating-integration-tests-apache-sling, however 6D Global Technologies decided to remove my authoring credit which seems unethical.

One of the lesser known features in Apache Sling Testing Tools framework is the SlingTestBase, this class can be extended to allow you to create tests against a Sling instance which will be automatically created, started and then shut down when the tests are complete.

Why Test this Way?

First, you should have some form of automatic testing on your Apache Sling and Adobe CQ5 projects.  Automatic testing gives you better coverage against regression and changes breaking code and ease the burden on the testers and quality assurance teams.  Additionally, by making your code testable, you are ensuring it follows coding best practices and is well structured.

So now you understand the need for testing on Sling and CQ5 projects, why not just use unit tests?  Well jUnit is great, and you can do some great unit tests with jmock or the built in Sling Mock Classes, but it’s easy to end up testing your ability to mock what you think will happen instead of testing your code.  Especially when dealing with the OSGi dependency injection.  Using mocks, there’s no good way to ensure your Service is defined correctly to be loaded in OSGi nor to ensure the third party dependencies will behave as you expect them to behave.

This is where integration testing comes in.  Using integration tests you can ensure your application will work within the Apache Sling framework.  There are actually a couple other other methods for invoking integration tests in Apache Sling, such as the Remote Tests however these tests require you to install testing code into the repository and are not easily automatically tested. The SlingTestBase allows you to create tests where the code is not installed and which automatically run with your build, once the code is compiled and packaged.

Creating an Integration Test

So now that you see why you will want to write integration tests for your project, how do you start?  The process of creating integration tests using the SlingTestBase begins with updating your POM.

Updating your POM

First you will need to add the following plugins to your project’s Maven POM.  The part each plugin acts in the process is described below the plugin configuration.

<plugin>
	<groupId>org.apache.maven.plugins</groupId>
	<artifactId>maven-dependency-plugin</artifactId>
	<executions>
		<execution>
			<id>copy-runnable-jar</id>
			<goals>
				<goal>copy-dependencies</goal>
			</goals>
			<phase>package</phase>
			<configuration>
				<outputDirectory>${project.build.directory}/sling</outputDirectory>
				<includeArtifactIds>org.apache.sling.launchpad</includeArtifactIds>
				<excludeTransitive>true</excludeTransitive>
				<overWriteReleases>false</overWriteReleases>
				<overWriteSnapshots>false</overWriteSnapshots>
			</configuration>
		</execution>
		<execution>
			<id>copy-dependencies</id>
			<goals>
				<goal>copy-dependencies</goal>
			</goals>
			<phase>package</phase>
			<configuration>
				<outputDirectory>${project.build.directory}/sling/additional-bundles</outputDirectory>
				<!-- Include artifact id's of all of the bundles to install here -->
				<includeArtifactIds>jstl</includeArtifactIds>
				<excludeTransitive>true</excludeTransitive>
				<overWriteReleases>false</overWriteReleases>
				<overWriteSnapshots>false</overWriteSnapshots>
			</configuration>
		</execution>
	</executions>
</plugin>

The copy-dependencies Maven Dependency plugin will copy the bundles you need to install for your integration tests from your local Maven Repository to the filesystem. Additionally, the copy-runnable-jar execution will copy the Sling Launchpad jar into your target directory so it can be executed.

<plugin>
	<artifactId>maven-antrun-plugin</artifactId>
	<executions>
		<execution>
			<phase>package</phase>
			<configuration>
				<tasks>
					<copy
						file="${project.build.directory}/${project.build.finalName}.jar"
						toDir="${project.build.directory}/sling/additional-bundles"
						verbose="true" />
				</tasks>
			</configuration>
			<goals>
				<goal>run</goal>
			</goals>
		</execution>
	</executions>
</plugin>

The AntRun Plugin used to copy the output of the built into the additional-bundles folder to be installed into the Sling testing instance when it starts. If your integration tests are part of a separate testing project this will not be required.

<plugin>
	<groupId>org.apache.maven.plugins</groupId>
	<artifactId>maven-failsafe-plugin</artifactId>
	<version>2.12.4</version>
	<executions>
		<execution>
			<id>integration-test</id>
			<goals>
				<goal>integration-test</goal>
			</goals>
		</execution>
	</executions>
	<configuration>
		<includes>
			<include>**/*IT.java</include>
		</includes>
		<systemPropertyVariables>
			<jar.executor.work.folder>${project.build.directory}/sling</jar.executor.work.folder>
			<jar.executor.jar.options>-p $JAREXEC_SERVER_PORT$</jar.executor.jar.options>
			<jar.executor.vm.options>-Xmx512m</jar.executor.vm.options>
			<jar.executor.jar.folder>${project.build.directory}/sling</jar.executor.jar.folder>
			<jar.executor.jar.name.regexp>org.apache.sling.launchpad.*jar$</jar.executor.jar.name.regexp>
			<additional.bundles.path>${project.build.directory}/sling/additional-bundles</additional.bundles.path>
			<keepJarRunning>false</keepJarRunning>
			<server.ready.timeout.seconds>60</server.ready.timeout.seconds>
			<sling.testing.timeout.multiplier>1.0</sling.testing.timeout.multiplier>
			<server.ready.path.1>/:script src="system/sling.js"</server.ready.path.1>
			<start.bundles.timeout.seconds>30</start.bundles.timeout.seconds>
			<bundle.install.timeout.seconds>20</bundle.install.timeout.seconds>
			<!-- 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>sample-sling-integration-test</sling.additional.bundle.1>
			<sling.additional.bundle.2>jstl</sling.additional.bundle.2>
		</systemPropertyVariables>
	</configuration>
</plugin>

The Maven Failsafe plugin defines all of the properties and runs the tests. Note that the bundles to install must be defined here as well. The properties used to configure the SlingTestBase are:

SlingTestBase Properties

  • test.server.url - The url for server, only needed if using an already running instance and the test.server.hostname is not set
  • test.server.username - The username to use for the server, only needed if using an already running instance
  • test.server.password - The password to use for the server, only needed if using an already running instance
  • server.ready.timeout.seconds - The number of seconds to wait before checking if the server is ready.
  • server.ready.path.[num] - A list of paths to check if the server is ready, can also contain a string to check for in the format: [path]:[string]
  • keepJarRunning - A flag of whether or not to shutdown the server when complete, generally used if the server should already be running
  • test.server.hostname - The test server hostname
  • additional.bundles.path - The path to the additional bundles to install once the Sling instance is started
  • sling.additional.bundle.[num] - The artifact id’s of the additional bundles to install
  • start.bundles.timeout.seconds - The timeout to wait for a bundle to start, depending on the performance of your system this may need to change
  • bundle.install.timeout.seconds - The timeout to wait for a bundle to install, again this may need to change depending on the performance of your system

JarRunner Properties

  • jar.executor.server.port - The server port on which the jar will be started
  • jar.executor.jar.folder - The folder containing the jar to run
  • jar.executor.jar.name.regexp - a regular expression for finding the name of the jar to run, the first jar in the jar folder matching this pattern will be executed
  • jar.executor.vm.options - Options to pass to the VM when executing the jar
  • jar.executor.work.folder - The work folder for the jar to be executed within, generally should be a subfolder of your target folder
  • jar.executor.jar.options - Options to pass to the jar being executed

 

Next, add the following dependencies:

	<dependency>
		<groupId>org.apache.sling</groupId>
		<artifactId>org.apache.sling.commons.testing</artifactId>
		<version>2.0.12</version>
		<scope>test</scope>
	</dependency>
	<dependency>
		<groupId>org.apache.sling</groupId>
		<artifactId>org.apache.sling.testing.tools</artifactId>
		<version>1.0.6</version>
		<scope>test</scope>
	</dependency>
	<dependency>
		<groupId>org.apache.sling</groupId>
		<artifactId>org.apache.sling.launchpad</artifactId>
		<version>6</version>
		<classifier>standalone</classifier>
		<scope>test</scope>
	</dependency>

These dependencies include the Sling Launchpad, which you will use to run the tests and some of the Sling testing APIs which help you to be able to run the tests.

Integration Test Class

Once you have your POM configured, go ahead and create a class to execute the integration tests. This class must extend the class SlingTestBase, for example:

public class SampleIT extends SlingTestBase {

	/**
	 * The SlingClient can be used to interact with the repository when it is
	 * started. By retrieving the information for the Server URL, username and
	 * password, the Sling instance will be automatically started.
	 */
	private SlingClient slingClient = new SlingClient(this.getServerBaseUrl(),
			this.getServerUsername(), this.getServerPassword());

	/**
	 * Execute before the actual test, this will be used to setup the test data
	 * 
	 * @throws Exception
	 */
	@Before
	public void init() throws Exception {
		[..Initialize The Tests...]
	}

	/**
	 * The actual test, will be executed once the Sling instance is started and
	 * the setup is complete.
	 * 
	 * @throws Exception
	 */
	@Test
	public void testSample() throws Exception {
		[..Run The Tests, any method annotated with @Test will be executed...]
	}
}

The test will be executed as any other jUnit test, the key things to know are that that the creation of the SlingClient will trigger the startup of the Sling instance and that with the configuration specified in the provided POM, the Sling instance will be shutdown when the test is complete. This can be changed by changing the keepJarRunning flag.

Initializing the Test

The SlingClient is simply a helper class allowing you to easily perform common tasks needed for intializing components, creating sample content, etc. A sample initialization method may look like the following:

log.info("init");

log.info("Creating testing component...");
if (!slingClient.exists("/apps/test")) {
	slingClient.mkdir("/apps/test");
}
if (!slingClient.exists("/apps/test/sample-test")) {
	slingClient.mkdir("/apps/test/sample-test");
}
slingClient.upload(
		"/apps/test/sample-test/sample-test.jsp",
		SampleIT.class.getClassLoader().getResourceAsStream(
				"sample-test.jsp"), -1, true);
log.info(getRequestExecutor()
		.execute(
				getRequestBuilder().buildGetRequest(
						"/apps/test/sample-test.3.json")
						.withCredentials("admin", "admin"))
		.assertStatus(200).getContent());

log.info("Creating testing content...");
if (slingClient.exists("/content/test/sample-test")) {
	slingClient.delete("/content/test/sample-test");
}
slingClient.createNode("/content/test/sample-test", "jcr:primaryType",
		"nt:unstructured", "sling:resourceType", "test/sample-test");

log.info(getRequestExecutor()
		.execute(
				getRequestBuilder().buildGetRequest(
						"/content/test/sample-test.json")
						.withCredentials("admin", "admin"))
		.assertStatus(200).getContent());

log.info("Initialization successful");

This sample creates the folders /apps/test/sample-test and then uploads the script sample-test.jsp into the folder then creates a sample content node in /content/test/sample-test and validates both have been created successfully.

Executing Tests

Also note from the initializaton section, that the validation of the content and script creation is done by creating and executing a GET request with the RequestBuilder and RequestExecuter classes. Using these classes is the easiest way to execute your tests. Using these classes you can execute requests against sample scripts and servlets to verify components and services execute correctly.

In the sample test, I am just executing a single reqest against the sample node and ensuring the response is what I expect.

log.info("testSample");

log.info("Executing content check");

RequestExecutor result = getRequestExecutor().execute(
		getRequestBuilder().buildGetRequest(
				"/content/test/sample-test.html").withCredentials(
				"admin", "admin"));
log.info(result.getContent());
result.assertStatus(200).assertContentContains("All Tests Succeeded");

log.info("testSample - TEST SUCCESSFUL");

Once you execute the request, you can verify the execution with the following methods:

  • assertContentContains - Assert that the content contains the specified String(s)
  • assertContentRegexp - Assert that the content contains the specified Regular Expression String(s)
  • assertContentType - Assert that the reponse has a certain content type (i.e. application/json)
  • assertStatus - Assert that the response status is a certain status code

You can also use the methods getResponse or getContent to examine the results directly.

Creating a Script

Once you have the test execution class created, you will want to probably create custom scripts to execute the tests. You could also execute tests directly against the application scripts or servlets.

For the sample I created a very simple script which simply calls a sample service and verifies that the result is the expected value.

<%@page session="false" contentType="text/html; charset=utf-8"%>
<%@page import="com.sixdimensions.wcm.cq.sampleintegration.HelloResourceService" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %><%@taglib prefix="sling" uri="http://sling.apache.org/taglibs/sling/1.0"%>
<sling:defineObjects/>
<%
HelloResourceService helloSvc = sling.getService(HelloResourceService.class);
pageContext.setAttribute("helloTxt",helloSvc.sayHello(resource));
%>
<!-- Some Testing here -->
<c:choose>
	<c:when test="${helloTxt == 'Hello sample-test!'}">
		All Tests Succeeded
	</c:when>
	<c:otherwise>
		Test Failures
	</c:otherwise>
</c:choose>

Running the Integration Test

By default the integration test will bind to the intgration-test Maven phase which will be run after the code is compiled and packaged. To execute the integration test, you can simply execute:

mvn clean install

In your project directory. This will call the integration tests and assuming everything is correct install the project output in your local Maven Repository.

Wrap Up

Hopefully you found this post useful.  One of the things I have been kicking around is that there’s really no reason you couldn’t do something similar using the Adobe CQ JAR instead of the Apache Sling JAR.  I have not gotten it quite working yet, but if I do, I will send an update.

Additionally, since this process takes a non-insignficant amount of time, it may be a good idea to wrap the integration tests in a profile and execute them when desired or as a part of the Continuous Integration process.

Finally, you can find a complete sample project on Six Dimensions’s GitHub.


Tags


comments powered by Disqus