In this article I'd like to describe how you write unit tests for code that accesses a JCR repository.
At first I really tried to mock the JCR API using Mockito, but stopped my attempt at the point where I had to mock the behavior of the Query Object Model. It became apparent, that writing mocks would outweigh the effort to write the actual production code by far. So I had to search for an alternative and found one.
The reference implementation of JCR is the Apache Jackrabbit project. This implementation comes with a set of JCR Repository implementations, one of these is the TransientRepository. The TransientRepository starts the repository on first login and shuts it down on the last session being closed. The repository is created in memory which works pretty fast and makes it the best solution for unit testing. But nevertheless, a directory structure is created for the repository and unless not specified a config file is created as well.
For writing unit tests against this repository, we need the following:
- a temporary directory to locate the directory structure of the repository
- a configuration file (unless you want one created on every startup)
- the repository instance
- a CND content model description to initialize the repository data model (optional)
- an admin session to perform administrator operations
- a cleanup operation to remove the directory structure
- the maven dependencies to satisfy all dependencies
<properties> <!-- JCR Spec --> <javax.jcr.version>2.0</javax.jcr.version> <!-- JCR Impl --> <apache.jackrabbit.version>2.6.5</apache.jackrabbit.version> </properties> ... <dependencies> <!-- The JCR API --> <dependency> <groupId>javax.jcr</groupId> <artifactId>jcr</artifactId> <version>${javax.jcr.version}</version> </dependency> <!-- Jackrabbit content repository --> <dependency> <groupId>org.apache.jackrabbit</groupId> <artifactId>jackrabbit-core</artifactId> <version>${apache.jackrabbit.version}</version> <scope>test</scope> </dependency> <!-- Jackrabbit Tools like the CND importer --> <dependency> <groupId>org.apache.jackrabbit</groupId> <artifactId>jackrabbit-jcr-commons</artifactId> <version>${apache.jackrabbit.version}</version> <scope>test</scope> </dependency> </dependencies>
Now let's create the directory for the repository. I recommend to locate it in a temporary folder so multiple test runs don't affect each other if cleanup failed. We use the Java TempDirectory facility for that:
//prefix for the repository folder import java.nio.file.Files; import java.nio.file.Path; ... private static final String TEST_REPOSITORY_LOCATION = "test-jcr_"; ... final Path repositoryPath = Files.createTempDirectory(TEST_REPOSITORY_LOCATION);
Next, you require a configuration file. If you already have a configuration file available in the classpath, i.e. in src/test/resource, you should load it first:
final InputStream configStream = YourTestCase.class.getResourceAsStream("/repository.xml");
Knowing the location and the configuration, we can create the repository:
import org.apache.jackrabbit.core.config.RepositoryConfig; import org.apache.jackrabbit.core.TransientRepository; ... final Path repositoryLocation = repositoryPath.toAbsolutePath(); final RepositoryConfig config = RepositoryConfig.create(configStream, repositoryLocation.toString()); final TransientRepository repository = new TransientRepository(config);
If you ommit the config parameter, the repository is created in the working directory including the repository.xml file, which is good for a start, if you have no such file.
Now that we have the repository, we want to login to create a session (admin) in order to populate the repository. Therefore we create the credentials (default admin user is admin/admin) and perform a login:
final Credentials creds = new SimpleCredentials("admin", "admin".toCharArray()); final Session session = repository.login(creds);
With the repository running and an open session we can initialize the repository with our content model if require some extensions beyond the standard JCR/Jackrabbit content model. In the next step I import a model defined in the Compact Node Definition (CND) Format, described in JCR 2.0
import org.apache.jackrabbit.commons.cnd.CndImporter; ... private static final String JCR_MODEL_CND = "/jcr_model.cnd.txt"; ... final URL cndFile = YourTestCase.class.getResource(JCR_MODEL_CND); final Reader cndReader = new InputStreamReader(cndFile.openStream()); CndImporter.registerNodeTypes(cndReader, session, true);
All the code examples above should be performed in the @BeforeClass annotated method so that the repository is only created once for the entire test class. Otherwise a lot of overhead will be generated. Nevertheless, in the @Before and @After annotated methods, you should create your node structures and erase them again (addNode() etc).
Finally, after you have performed you test, you should cleanup the test environment again. Because a directory was created for the transient repository, we have to remove the directory again, otherwise the temp folder will grow over time.
There are three options for cleaning it up.
- Cleaning up in @AfterClass annotated method
- Cleaning up using File::deleteOnExit()
- Cleaning up using shutdown hook
import org.apache.commons.io.FileUtils; ... @AfterClass public static void destroyRepository(){ repository.shutdown(); String repositoryLocation = repository.getHomeDir(); try { FileUtils.deleteDirectory(new File(repositoryLocation)); } catch (final IOException e) { ... } repository = null; }
As fail-safe operation I prefer to add an additional shutdown hook that is executed when the JVM shuts down. This will delete the repository even when the @AfterClass method is not invoked by JUnit. I do not use the deleteOnExit() method of File as it requires the directory to be empty while I could call any code in the shutdown hook using my own cleanup implementation.
A shutdown hook can easily be added to the runtime by specifying a Thread to be executed on VM shutdown. We simply add a call to the destroy methode to the run() method.
Runtime.getRuntime().addShutdownHook(new Thread("Repository Cleanup") { @Override public void run() { destroyRepository(); } });
Now you should have everything to set-up you Test JCR Repositoy and tear-down the test environment. Happy Testing!