Monday, July 11, 2011

Mocking LDAP Servers for JUnit

As I have gone over before, I am a fan of unit testing. The list of benefits is quite extensive. However, one of the chief hurdles of unit testing is "I don't know how to test that...". I ran into just such a problem the other day. I have a project that utilizes an LDAP server for it's back end datastore. While support for databases in unit tests is a fairly flushed out field, support for LDAP servers is not-so-much.

Luckily, though, the good people over at UnboundID have a good solution to help us out. Their LDAP SDK for Java has been a very handy tool I have used for quite a while for handling all of my LDAP connections. Recently, with their 2.1.0 release, they unveiled a new feature specifically for this issue - the in-memory directory server. However, attempting to implement this in-memory server was a little bit more challenging than the documentation would make it seem. There are just a few gotchas to be aware of.

The documentation does contain an example that shows the java code you need to get going on this. The part that is tricky, though, is this line:
server.initializeFromLDIF(true, "/tmp/test.ldif");
There are a couple of problems here. First off, it is, well, wrong. The actual method name is not initializeFromLDIF, but rather importFromLDIF. So it should look like:
server.importFromLDIF(true, "/tmp/test.ldif");
The second, and slightly more significant problem, is that there is no documentation on exactly what needs to go into test.ldif. You are left to figure that one out on your own.

The ldif file that you use to import MUST contain the definition for whatever value you specify as the base DN in the InMemoryDirectoryServerConfig constructor. It may not contain any definitions of the parents of the base DN (which makes sense, being the BASE DN), and it may not specify the definition of further children without specifying the definition of the base DN.

So, to go along with the example, here is a sample LDIF you can use to get you started:
dn: dc=example,dc=com
objectClass: top
objectClass: domain
dc: example
That is the minimum required definition of the base DN. Note that the following LDIF does NOT work:
dn: dc=com
objectClass: top
objectClass: domain
dc: com

dn: dc=example,dc=com
objectClass: top
objectClass: domain
dc: example

If you want to specify any further branches, that may be done in the same LDIF file, so long as you still include the top portion as above. For example, if you have a People and a Groups branch, your initial LDIF may look something like this:
dn: dc=example,dc=com
objectClass: top
objectClass: domain
dc: example

dn: ou=People,dc=example,dc=com
objectClass: top
objectClass: organizationalUnit
ou: People

dn: ou=Groups,dc=example,dc=com
objectClass: top
objectClass: organizationalUnit
ou: Groups

So, that covers the simple example test case, but I'm guessing that is not enough for most people. More than likely, you already have a real LDAP server out there, and you are wanting to mimic that server in your In Memory instance, so you can test in an environment that matches your production environment.

That is not necessarily apparently easy right off the bat. You need to replicate your server's schema into the in memory one, replicate all of the branches that you use, and pull in some real data to test with.

As it turns out, replicating your existing server is not too terribly complicated. If you happen to have an LDIF file that defines your server's schema on hand, you can include that into your project, and the following code should load it up:
InputStream schemaLdif = this.getClass().getClassLoader().getResourceAsStream("schema.ldif");
Entry schemaEntry = new Entry(IOUtils.toString(schemaLdif, "UTF-8"));
Schema newSchema = new Schema(schemaEntry);
config.setSchema(newSchema);
The above lines would be inserted into the sample code from the UnboundID website, before the call to the InMemoryDirectoryServer constructor. Note that I have not actually performed the above method myself, as I do not have my server's schema LDIF handy.

For me, and many others out there, you may not have access to the schema LDIF file of your server. Luckily, their is an easier way. First you must actually make a connection to your server, so that you have an LDAPConnection object pointing to it. Then you use the following code snippet:
LDAPConnection connection = //however you get your connection object
newSchema = Schema.getSchema(connection);
config.setSchema(newSchema);
The advantages to this method are that you always have the most up to date server schema, and you don't have to worry about tracking down and storing a hard copy of your server's current schema. The down sides to this method are the dependence on your server being up and available, and it is a little slow - my testing took about 6-7 seconds to retrieve the schema.

Storing the schema you retrieve from the server in a static variable can help with the performance bite, as it would only have that 6-7 second delay for your first test. However, there is nothing you can do about the dependence on the external system when utilizing this method, so that is a personal choice you have to make.

Once you have the schema loaded, you still need to import the above LDIF file for your base DN, as well as any test data you might have. All of that can be accomplished with the above importFromLDIF call. Since you should be matching your server's schema exactly, you should be able to just export records directly from your server into an LDIF file, and import them directly into your in memory server.

Once you do that, you should be good to go, ready to execute your tests against a clean, save, local environment.

1 comment:

  1. Thanks for your post, and I'm glad you found the in-memory directory server useful.

    A couple of comments on your post:

    - Thanks for pointing out the discrepancy between initializeFromLDIF and importFromLDIF. The method name was originally initializeFromLDIF, but I changed it before releasing it. I have committed a change to use the correct name in the documentation.

    - The comment about "dc=com" not being acceptable is because you specified "dc=example,dc=com" as the base DN. If you had specified "dc=com" as the base DN, then you would have been allowed to include that entry.

    - If you have the desired schema in a file, then you could use one of the Schema.getSchema() methods to load it, and it will do all the necessary work. If you have an InputStream, then you could have used LDIFReader to read the entry from that stream. I can look at adding static methods in the LDIFReader class to make it more convenient to read entries from LDIF, like one that takes a path or input stream and returns a List of Entry objects (assuming that your LDIF is small enough that you don't have to worry about running out of memory). I have also just added new static LDIFReader.readEntries methods that can be used to read entries from an LDIF file or input stream into a list.

    - If you have any other problems or suggestions for improvement, feel free to send them to ldapsdk-support at unboundid.com or use the mailing lists or discussion forum at http://sourceforge.net/projects/ldap-sdk/.


    Neil Wilson
    Primary LDAP SDK Developer
    UnboundID Corp.

    ReplyDelete