Introduction
Automated unit tests are crucial for developing enterprise-quality code. Not only do they give us confidence that the code we write today is working according to the specified requirements, but they also give us confidence that our code continues to work when it is refactored.
JUnit is an open-source unit testing framework that has become the de facto choice of developers for writing their automated unit tests. This article assumes the reader is familiar with Junit. If you need to come up to speed, check out Mike Clarke’s concise, but thorough, JUnit Primer.
In this article we’ll look at some of the issues that typically prevent developers from testing their DFC code, we’ll explore how to use JUnit as a basis for writing automated unit tests for DFC code, and we’ll see how to use Ant to easily run the tests. While this article defines several Java interfaces in order to show how to design the framework, it does not include the implementation of those interfaces (however, Blue Fish customers do receive the framework).
The “Pain Points”
If unit testing is so great, why do so few DFC tests actually get written? The main issues are:
- Change is not easy. If you are having a problem getting your unit tests written, it may be because you have never written automated unit tests before. If you need some incentive to begin writing effective unit tests, then check out Mike Clarke’s JUnit Primer.
- Project schedule pressures often interfere with developers writing effective unit tests. This article cannot fully address that problem within your organization, but it can alleviate the pain by providing you with a mechanism that will cut down the amount of time it takes to write your tests. Still, you need to account for testing time when you schedule your project, and you must be diligent about writing the tests as you develop your code (instead of at the end of the project, when there is little or no time left).
- Authenticating users in order to perform DFC test operations requires that the test harness has some provision in place for managing user logins. Implementing such a provision is not difficult, but it is a sufficient barrier to entry for developers when it comes time to write tests, especially if the tests have been left to the end of the project. This article presents a simple mechanism for managing user logins.
- Setting up the state necessary in the docbase for a test to run can be a daunting task. The objects that may need to be created can include object types, object instances, ACLs, and relations (just to name a few). The JUnit framework provides a hook that allows such setup to occur before each test method is invoked. However, that hook does not know which test method is being executed, so it cannot know what needs to be done to setup state in the repository for each test. Instead, you must explicitly setup your state in each test so that your test can proceed. This article provides you with some basic mechanisms for setting up the state specific to your tests, but you may need to provide specialized calls for your tests.
- Like setup, tearing down the state necessary in the docbase for a test to run can be tricky. The JUnit framework provides a hook that allows such teardown to occur after each test method is invoked. However, that hook does not know which test method is being executed, so it cannot know what needs to be done to cleanup state in the repository for each test. Instead, you must explicitly teardown your state in each test so that your docbase does not become polluted with objects over time (keep in mind that automated unit tests are meant to be run quite frequently over a long period of time, to ensure that no new bugs have found their way into your code). This article provides you with a general mechanism for tearing down the state specific to your tests, but you may still have to do some special cleanup depending on the nature of your tests.
Logins
All DFC calls are made through authenticated connections to the Content Server. This implies that DFC unit tests need to be able to login to a docbase. If your DFC code has any security requirements, then the unit tests will most likely need to be executed as various logins, in order to test positive and negative test instances. While these logins can be hardcoded, it is much more desirable to have them configured in an external file that is used by the testing framework.
It is also useful to introduce a layer of abstraction above the user OS names that are used to authenticate Documentum users. This abstraction allows the test to refer to a login as a logical name, such as “admin” or “readonly-user”, instead of the actual user OS names. You can think of these logical names as testing roles. The tests are written in terms of these logical roles, which are mapped to actual users in a configuration file. This allows the tests to be easily run against various docbases with different users who act as the same logical user.
IAuthenticator
The IAuthenticator interface is responsible for authenticating logical users and returning IDfSession and IDfSessionManager objects, which can then be used to perform DFC actions. An IAuthenticator provides the following methods:
Method | Return Value |
getSession(logicalName) | An IDfSession authenticated with the credentials and docbase identified in the configuration file for the logical user. |
getSessionManager(logicalName) | An IDfSessionManager with the credentials identified in the configuration file for the logical user. |
getDocbase(logicalName) | The name of the docbase that is associated with the logical user in the configuration file. |
Note:
Just like all other DFC code, you must release all IDfSession objects back to their IDfSessionManager, or you will leak sessions. The convenience method DfcTestBase.release(IDfSession) makes this easy to do.
DfcTestBase.createAuthenticator()
The DfcTestBase class implements IAuthenticator, allowing the test methods in a subclass to obtain sessions. DfcTestBase implements these methods by delegating to the IAuthenticator returned by the abstract method DfcTestBase.createAuthenticator(), which your subclass of DfcTestBase must implement.
Configuration of Logins
In order for DfcTestBase.createAuthenticator() to be implemented, it will need to have some login credentials for actual Documentum users, each set of which is associated to a logical user name. So, for each logical user that you require in a test, you will need to define the following values in order to authenticate a Documentum connection:
- the user OS name
- the password
- the domain (optional)
- the repository
How and where you store this information is a decision for your development team. At Blue Fish, we store the values in an XML file, with the passwords optionally being encrypted using some encryption scheme. A typical configuration might look like:
<?xml version="1.0" encoding="UTF-8"?>
<logins>
<login logicalName="read-only" userOSName="readonly" password="password" repository="test_53" />
<login logicalName="admin" userOSName="dmadmin" password="password" repository="test_53" />
</logins>
In order to keep the processing of the configuration simple, you might choose to represent the same data as a file that can be read using a java.util.Properties Object. The following example is one such approach, where each line starts with the logical name:
# The login info for the admin user
admin.userOSName=dmadmin
admin.password=password
admin.repository=test_53
# The login info for the read-only user
read-only.userOSName=readonly
read-only.password=password
read-only.repository=test_53
Specifying the Login Configuration Location
Your implementation of DfcTestBase.createAuthenticator() should resolve the login configuration and load it. Once again, how this happens is up to your development team. However, you should keep in mind that your unit tests are going to be executed in a variety of ways (via Ant, via an IDE, etc.), so you should make your framework flexible. At Blue Fish, our DfcTestBase looks for a System property as defined in the table below in order to resolve the logins (if more than one of the properties are defined, then the first one found in the table wins):
System Property Name | Property Value |
dfc.logins.filename | The name of the file containing the logins. Use this if your configuration is in a file. |
dfc.logins.resource | The name of the co-located resource containing the logins. Use this if your configuration is packaged in a JAR file, or is resolvable within a CLASSPATH. |
dfc.logins.url | The URL for the logins. Use this if your configuration is being managed by some server, such as an HTTP server. |
Using System properties is also useful when executing your tests via Ant. Your Ant
build file can pass these Ant
properties straight thru to the test class that it runs, so you can specify these in your build.properties file. For example, the following entry in your build.properties file will specify that the logins are located in the file c:/test/logins.ini:
# The logins are located in the file c:/test/logins.ini
dfc.logins.filename=c:/test/logins.ini
DfcTestBase provides several convenience methods to help you create cabinets and objects in order to setup state in the docbase. These are quite generic, perhaps so much so that you will not find them very valuable when writing your unit tests. If that is the case, your subclass of DfcTestBase should provide some specialized factory methods that create the objects that you need for your tests.
Creating Random Names
When you are running your tests, it is generally a bad idea to hardcode object names into the test, especially for folders and cabinets. Doing so will prevent multiple instances of the test from running simultaneously against the same docbase (because you cannot have two folders with the same name linked under the same parent). The DfcTestBase class has a method genRandomName() that will generate a random name that is suitable for use as an object name, including ACL names and folder names.
Creating Test Cabinets
Any IDfSysObjects that your tests create need to be linked somewhere. If possible, it is useful to link all the objects for a test under a single cabinet, which we’ll call a “test cabinet”. This makes the tear down for the test simple, because the test cabinet can simply be deep-deleted, meaning the cabinet and all of it sub-folders and objects are deleted. The DfcTestBase class provides two methods for creating cabinets:
- createTestCabinet(IDfSession session) will create a cabinet of type dm_cabinet with a random name. This is useful if your are just creating a cabinet to contain a number of objects and you don’t need to specify the name or type.
- createTestCabinet(IDfSession session, String name, String type) will create a cabinet with the specified name and type. This is useful when you need to control the name and/or the type of the cabinet.
Note:
The createTestCabinet() methods automatically queue up the new cabinet for deletion (see Tearing Down State).
Creating Test Objects
The DfcTestBase class provides one method for creating general IDfSysObjects:
- createTestObject(IDfSession session, String type, IDfFolder folder) will create an instance of the specified object type, link it under the folder, and save the object.
Note:
The createTestObject() method DOES NOT automatically queue up the new cabinet for deletion (see Tearing Down State). This is because these objects are typically linked under a test cabinet that will be deep-deleted, so they do not need to be queued up for deletion as well.
Tearing Down State
ISmartDeleter
You don’t want your tests populating your docbases with test objects that are useless after the execution of the tests. In order to prevent this from happening, the interface ISmartDeleter has been defined. This simple interface provides two methods:
- queueObjectForDeletion(Object) will queue a generic Object for deletion later
- deleteQueuedObjects() will delete all the queued objects
DfcTestBase.createSmartDeleter()
The DfcTestBase class implements ISmartDeleter, allowing the test methods in a subclass to queue objects for deletion. DfcTestBase implements these methods by delegating to the ISmartDeleter returned by the abstract method DfcTestBase.createSmartDeleter(), which your subclass of DfcTestBase must implement. You should consider the following issues when implementing ISmartDeleter:
- The order of deletion does matter. In general, you should probably delete objects in the order they were queued. However, you may find there are special cases where you need to delay deleting an object until some other objects have been deleted. Your implementation will have to take this into consideration.
- The queueObjectForDeletion() takes a generic Java Object. Your implementation of ISmartDeleter should perform some type checking on the Object in order to determine what to do with it. For example, if the Object is IDfSysobject, an IDfFolder, an IDfVirtualDocumentNode, or an IDfVirtualDocument, then you can use a DFC IDfDeleteOperation to perform the deletion of the object (this is especially useful for the deep-deletion of folders and virtual documents). Other types of DFC objects, like IDfACL and IDfType, will have to be handled on a case-by-case basis (see the DFC API javadoc for IDfPersistentObject to see other types that you may need to handle in a special manner).
- Your implementation of deleteQueuedObjects() should try to process all the Objects queued for deletion, even if errors are encountered while processing any individual Object. To do this, be sure to put a try/catch block inside the loop that is traversing the queued Objects. For example:
public void deleteQueuedObjects(IDfSession session) throws Exception {
Exception lastException = null;
Iterator iter = _queue.iterator();while (iter.hasNext()) {
try {
Object objectToDelete = iter.next();// Put your special logic here to handle deletion of the Object,
// probably based on the type of the Object.
} catch (Exception e) {
// Keep track of last exception, so we can throw at the end
lastException = e;
}
}// Throw the last exception encountered
if (lastException != null) {
throw lastException;
}
}
If you are thinking that implementing ISmartDeleter appears to be the most complicated part of the framework, then you are correct. This is one of the reasons that developers shy away from writing DFC unit tests. However, once you have written an implementation, you will be able to reuse it across projects.
DfcTestBase.getTearDownUser()
Your subclass of DfcTestBase must implement the abstract method getTearDownUser(), which returns the logical name of the user that is used to delete the queued objects in tearDown(). That user should be a super user.
DfcTestBase.tearDown()
DfcTestBase redefines JUnit‘s tearDown() so that it can invoke deleteQueuedObjects(), thereby deleting all of the objects that have been queued for deletion by a test method. The implementation looks like:
/**
* Redefine tearDown() to delete all the objects queued for deletion.
* The delete operations will be performed using the logical user
* returned by <code>getTearDownUser()</code>.
*/
protected void tearDown() throws Exception {
IDfSession session = null;
try {
// Acquire session for teardown
session = getSession(getTearDownUser());
// Delete everything that has been queued
deleteQueuedObjects(session);
} finally {
release(session);
}
}
JUnit automatically invokes tearDown() after it invokes a test method, so you don’t need to worry about calling tearDown(). Instead, just queue up the objects you create in your test method using queueObjectForDeletion(), and they will be automatically deleted.
Writing Your Tests
Now that we have seen what the DfcTestBase class gives us, let’s see how to put all the pieces together to write an actual unit test. There is a class named CabinetCreator located in the same package as DfcTestBase. That class has a single method createCabinet() that takes a number of parameters and will create a cabinet. We want to write a collection of tests that assert the code operates as it says it does. To do this, we have the class CabinetCreatorTest, which is also in the same package. The CreateCabinetTest class extends DfcTestBase and has several public test methods that JUnit will automatically execute.
Note:
The CabinetCreator would typically live in a different directory structure than your tests. I have put it in the test directory to simplify the build process.
Note:
The CabinetCreatorTest class is still abstract because it does not implement the three abstract methods in DfcTestBase. However, the various testXYZ() methods in CabinetCreatorTest will give you a feel for how to write automated tests for DFC.
Set Up a Test Docbase
You do not want to be running your automated unit tests against your production docbase. So, get a dedicated docbase set up just for your unit tests. If necessary, you can safely blow this docbase away and recreate it without affecting your production environment.
Implement IAuthenticator
Decide on how you want to represent your login configuration data (XML, INI properties, database, …) and provide an implementation of IAuthenticator. Hopefully, this implementation can be reused across projects within your organization, so you will only need to do this once.
Implement ISmartDeleter
Provide an implementation of ISmartDeleter based on the types of Objects that your test code is creating. Hopefully, this implementation can be reused across projects within your organization, so you will only need to do this once.
Subclass DfcTestBase
You want your test case to subclass DfcTestBase, which in turn subclasses junit.framework.TestCase. More than likely, you can write a subclass that will serve as the superclass for each of your test classes. That class should implement the following abstract methods:
- createAuthenticator(): will resolve the login configuration and use it to contruct your implementation of IAuthenticator
- createSmartDelete(): will return an instance of your implementation of ISmartDeleter
- getTearDownUser(): will return the name of a logical user that will be used for tearDown()
Define Your Login Configuration
Determine how many logical users you will need in your tests and put together the appropriate configuration data, as required by your implementation of IAuthenticator.
Write Your Public Test Methods
JUnit will automatically invoke all the public methods in your test class that start with “test”. So, identify what conditions you want to test and construct your test methods accordingly. I typically create several test methods for a single piece of functionality that is being tested, where each test method tests a different aspect of the functionality. For instance, I will create a test method that checks that the code does what it says it should when passed null parameters. I will write a separate test for the “happy path” of the functionality. And I will create tests that test the edge cases of the functionality. See the code for CreateCabinetTest for examples of this.
The following code sample shows the typical structure for a test method. A session is acquired, some objects are created and queued for deletion, some tests are performed, and the session is released.
public void testObjectCreation() throws Exception {
IDfSession session = null;
try {
// Acquire a session as the logical "account-manager" user
session = getSession("account-manager");
// Create a test cabinet. This will be randomly named and
// will be automatically queued for deletion.
IDfFolder testCabinet = createTestCabinet(session);
// Create a test object. This will be randomly named and
// will be linked under the test cabinet. Because the cabinet
// will be cleaned up, the test object in the cabinet will be
// deleted as well.
IDfSysObject testObject = createTestObject(session, "dm_document", testCabinet);
// Do some things with the test object
} finally {
release(session);
}
}
Using Ant To Run Your Tests
The best way to run your unit test is using Ant. The download contains a basic build.xml build file that can be used as a template for your own build file. The build file requires several build properties to be set so that it can locate the DFC JARs and dynamic libraries. These properties are all documented in the build.properties file in the download. Be sure to edit build.properties to reflect the state of your machine before running your tests.
Downloads / Resources
The source code and build files for this article are available for download as one ZIP:
If you want to just scan the Java code and build files, here are some links:
- DfcTestBase
- IAuthenticator
- ISmartDeleter
- TestUtils
- CabinetCreator
- CabinetCreatorTest
- Ant build properties
- Ant build file
In addition, the following links provide useful information related to this article:
- Junit – The official JUnit website
- Junit FAQ – Frequently asked questions and answers for JUnit
- Test First – Test Driven Development using JUnit
- Junit Primer – Mike Clarke’s primer for JUnit
- Ant – The official Ant website
- Ant FAQ – Frequently asked questions and answers for Ant
Conclusion
Although automated testing of DFC applications can be tricky, it’s not as bad as it seems. We have shown you a basic framework that you can extend upon to build effective unit tests for your DFC code. The framework is designed around the premise that a DFC test harness should leave your repositories in the same state that it found them, not pollute them with random objects that need to be purged later. Happy testing!