4.2. Using ApacheDS for unit tests has been edited by Emmanuel Lécharny (Dec 05, 2008).

(View changes)

Content:
Work in progress

This site is in the process of being reviewed and updated.

This page describes how to define your own unit tests, using the trunk. It won't work with 1.5.4 as there is a problem in the way we built the apacheds-all-1.5.4.jar. A workaround is to load all the included jars, but it's a bit painfull, as there are 32 jars to define (this list will be given at the end of this page}

Using ApacheDS for unit tests

The idea is to use ADS as an embedded server for Ldap junit tests. We will build an environment in which it will be convenient to test Ldap applications.

First steps

We have two choices : either we launch the server totally embedded, without having to communicate with it using the Ldap Protocol, or we want to use the server as a remote server, with Ldap protocol in the middle (it can be useful if one want to test specific applications)

Anyway, we will simply launch only one server (if one want to test referrals, it might be necessary to initialize 2 or more servers)

We are using JUnit 4.4

We have to define a layout for the files and directory we will use in this tutorial. Let's use the maven layout :

/
|
+--src
    |
    +--test
        |
        +--java      : we will put all the sources into this directory
        |
        +--resources : we will put the resources files into this directory

You will need some jars file in order to compile and run this sample when used with 1.5.4. If you are using the trunk, don't worry, a pom.xml with all the needed dependencies is attached.

* antlr-2.7.7.jar
* apacheds-bootstrap-extract-1.5.4.jar
* apacheds-bootstrap-partition-1.5.4.jar
* apacheds-core-1.5.4-sources.jar
* apacheds-core-avl-1.5.4.jar
* apacheds-core-cursor-1.5.4.jar
* apacheds-core-entry-1.5.4.jar
* apacheds-core-integ-1.5.4.jar
* apacheds-core-shared-1.5.4.jar
* apacheds-jdbm-1.5.4.jar
* apacheds-jdbm-store-1.5.4.jar
* apacheds-protocol-ldap-1.5.4-sources.jar
* apacheds-protocol-shared-1.5.4.jar
* apacheds-schema-bootstrap-1.5.4.jar
* apacheds-schema-registries-1.5.4.jar
* apacheds-server-integ-1.5.4.jar
* apacheds-utils-1.5.4.jar
* apacheds-xdbm-base-1.5.4.jar
* apacheds-xdbm-search-1.5.4.jar
* bcprov-jdk15-140.jar
* commons-collections-3.2.1.jar
* commons-io-1.3.2.jar
* commons-lang-2.4.jar
* junit-4.4-sources.jar
* log4j-1.2.14-sources.jar
* mina-core-1.1.7.jar
* mina-filter-ssl-1.1.7.jar
* shared-asn1-0.9.13.jar
* shared-asn1-codec-0.9.13.jar
* shared-ldap-0.9.13.jar
* shared-ldap-constants-0.9.13.jar
* slf4j-api-1.5.2-sources.jar
* slf4j-log4j12-1.5.2-sources.jar

Creating a blank test

So let's declare the framework for a simple unit test, using a server which will be used through socket (the first free port above 1023 will be used). This first test will do a search operation on a server.

package org.apache.directory.server.unittests.search;

import static org.apache.directory.server.integ.ServerIntegrationUtils.getWiredContext;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import javax.naming.NamingEnumeration;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.LdapContext;

import org.apache.directory.server.core.integ.Level;
import org.apache.directory.server.core.integ.annotations.ApplyLdifs;
import org.apache.directory.server.core.integ.annotations.CleanupLevel;
import org.apache.directory.server.integ.SiRunner;
import org.apache.directory.server.ldap.LdapService;
import org.junit.Test;
import org.junit.runner.RunWith;


/**
 * Test a single search.
 */
@RunWith ( SiRunner.class ) 
@CleanupLevel ( Level.CLASS )
@ApplyLdifs( {
    
    // The added entry
    "dn: cn=Kim Wilde,ou=system\n" +
    "objectClass: person\n" +
    "objectClass: top\n" +
    "cn: Kim Wilde\n" +
    "sn: Wilde\n\n"
    }
)
public class SimpleTest
{


    private static final String BASE = "ou=system";
    public static LdapService ldapService;
    private static final String FILTER = "(objectclass=*)";


    /**
     * Test a simple search operation
     */
    @Test
    public void testSimpleSearch() throws Exception
    {
        LdapContext ctx = ( LdapContext ) getWiredContext( ldapService ).lookup( BASE );

        SearchControls ctls = new SearchControls();
        ctls.setSearchScope( SearchControls.OBJECT_SCOPE );
        ctls.setReturningAttributes( new String[]
            { "objectclass" } );
        String rdn = "cn=Kim Wilde";

        NamingEnumeration<SearchResult> result = ctx.search( rdn, FILTER, ctls );
        
        if ( result.hasMore() )
        {
            SearchResult entry = result.next();
            Attributes heatherReloaded = entry.getAttributes();
            Attribute loadedOcls = heatherReloaded.get( "objectClass" );
            assertNotNull( loadedOcls );
            assertTrue( loadedOcls.contains( "person" ) );
            assertTrue( loadedOcls.contains( "top" ) );
        }
        else
        {
            fail( "entry " + rdn + " not found" );
        }
    }
}

In order to have this test running, you will need to declare some libraries. The best solution is clearly to define a pom.xml file for that purpose. Here it is :

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>org.apache.directory.server</groupId>
  <artifactId>apacheds-unit-test</artifactId>
  <version>1.5.5-SNAPSHOT</version>
  <name>ApacheDS Server Unit</name>
  <packaging>jar</packaging>  

  <description>
    Unit test for ApacheDS Server JNDI Provider
  </description>

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.4</version>
      <scope>provided</scope>
    </dependency>

    <dependency>
      <groupId>log4j</groupId>
      <artifactId>log4j</artifactId>
      <version>1.2.14</version>
      <scope>provided</scope>
    </dependency>

    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-log4j12</artifactId>
      <version>1.5.2</version>
      <scope>provided</scope>
    </dependency>

    <dependency>
      <groupId>org.apache.directory.server</groupId>
      <artifactId>apacheds-core-integ</artifactId>
      <version>1.5.5-SNAPSHOT</version>
    </dependency>  

    <dependency>
      <groupId>org.apache.directory.server</groupId>
      <artifactId>apacheds-server-integ</artifactId>
      <version>1.5.5-SNAPSHOT</version>
    </dependency>  

    <dependency>
      <groupId>org.apache.directory.server</groupId>
      <artifactId>apacheds-all</artifactId>
      <version>1.5.5-SNAPSHOT</version>
    </dependency>
  </dependencies>

  <build>
    <pluginManagement>
      <plugins>
        <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-compiler-plugin</artifactId>
          <version>2.0.2</version>
          <configuration>
            <source>1.5</source>
            <target>1.5</target>

            <optimize>true</optimize>
            <showDeprecations>true</showDeprecations>
            <encoding>ISO-8859-1</encoding>
          </configuration>
        </plugin>
        <plugin>
          <artifactId>maven-surefire-plugin</artifactId>
          <configuration>
            <argLine>-Xmx1024m</argLine>
          </configuration>
        </plugin>
      </plugins>
    </pluginManagement>
  </build>
</project>

Is that it ? Pretty much. All you have to do now is to run the test, using a Java 5 JVM and Maven 2.0.9 :

#user:~/ws-ads-1.5.4/UnitTest$ mvn test
[INFO] Scanning for projects...
[INFO] ------------------------------------------------------------------------
[INFO] Building ApacheDS Server Unit
[INFO]    task-segment: [test]
[INFO] ------------------------------------------------------------------------
[INFO] [resources:resources]
[INFO] Using default encoding to copy filtered resources.
[INFO] [compiler:compile]
[INFO] No sources to compile
[INFO] [resources:testResources]
[INFO] Using default encoding to copy filtered resources.
[INFO] [compiler:testCompile]
[INFO] Nothing to compile - all classes are up to date
[INFO] [surefire:test]
[INFO] Surefire report directory: /home/elecharny/ws-ads-1.5.4/UnitTest/target/surefire-reports

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running SimpleTest
log4j:WARN No appenders could be found for logger (org.apache.directory.server.integ.SiRunner).
log4j:WARN Please initialize the log4j system properly.
Ldap service started.
Ldap service stopped.
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 5.114 sec

Results :

Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESSFUL
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 10 seconds
[INFO] Finished at: Fri Nov 21 15:40:32 CET 2008
[INFO] Final Memory: 11M/83M
[INFO] ------------------------------------------------------------------------
#user:~/ws-ads-1.5.4/UnitTest$

You have written your very first test using the test framework provided in ADS 1.5.5 !

Here are the test files :
SimpleTest.java
pom.xml

Test description

Ok, we have a running test, but when it comes to write your own, you need a bit more information. Let's try to explain the different parts of this test.

Annotations

The first interesting part is the annotations we are using. As we said, the server is launched automatically, and we are using the ChangeLog mechanism to restore the base in a pristine state between each test. This is done with annotations.

Annotation Parameter Default value Description
@RunWith SiRunner.class none Run the test with the defined Runner, instead of the default JUNIT one.
This is mandatory to add this annotation, as it determinate the way the server
will be clean when each test has run
@CleanupLevel level.CLASS Level.CLASS Define when we should start and stop the server. Usually,
we want to do that when entering the test class, but we may want to do that for each method
(using the Level.METHOD value ) or at the opposite, on the Suite level (Level.SUITE), or even on the global
level (Level.SYSTEM)
@ApplyLdifs A list of entries in a LDIF format none This is one of the most interesting new feature :
you can define the data you need to be present when the server is started, they will be injected
before the first test is run (it will also depend on the CleanupLevel).
@ApplyLdifFiles A list of files containing LDIF entries none Give sthe list of files to be injected
into the server before the tests are run.
@Factory A factory instanciating a server none This annotation helps you to define your own
server initialization. We haven't used one in the simple test.

Server startup

As you can see in the test, there is no place where you tell the server to be started. This is done by the SiRunner. When the tests are all done, the server is also automatically shutdown, and all the created files are removed.

The initialization will update the ldapService instance which has been added at the beginning of the code. You will use this instance to access to the underlying LDAP base.

You just have to focus on writing your own tests !

Writing your own test using JNDI

All the LDAP operation are supported, you just have to use the JNDI API to write your tests. The only difference is the way you get a context : you have to use one of those methods :

/**
 * Creates a JNDI LdapContext with a connection over the wire using the 
 * SUN LDAP provider.  The connection is made using the administrative 
 * user as the principalDN.  The context is to the rootDSE.
 *
 * @param ldapService the LDAP server to get the connection to
 * @return an LdapContext as the administrative user to the RootDSE
 * @throws Exception if there are problems creating the context
 */
public static LdapContext getWiredContext( LdapService ldapService )

/**
 * Creates a JNDI LdapContext with a connection over the wire using the 
 * SUN LDAP provider.  The connection is made using the administrative 
 * user as the principalDN.  The context is to the rootDSE.
 *
 * @param ldapService the LDAP server to get the connection to
 * @return an LdapContext as the administrative user to the RootDSE
 * @throws Exception if there are problems creating the context
 */
public static LdapContext getWiredContext( LdapService ldapService, Control[] controls )

/**
 * Creates a JNDI LdapContext with a connection over the wire using the 
 * SUN LDAP provider.  The connection is made using the administrative 
 * user as the principalDN.  The context is to the rootDSE.
 *
 * @param ldapService the LDAP server to get the connection to
 * @return an LdapContext as the administrative user to the RootDSE
 * @throws Exception if there are problems creating the context
 */
public static LdapContext getWiredContext( LdapService ldapService, String principalDn, String password )

That's it ! Everything else is just pure JNDI

Writing your own tests using the Netscape API

If you don't like JNDI, or prefer to use the Netscape API (LdapSdk-4.1), you have two major differences :

  • first you have to add this API jar into the dependencies in the pom.xml file
  • second you will use the getWiredConnection() method instead of the getWiredContext().

The API is :

public static LDAPConnection getWiredConnection( LdapService ldapService )
public static LDAPConnection getWiredConnection( LdapService ldapService, String principalDn, String password )

Both methods are similar to the getWiredContext which has been described before, except that they return a LdapConnection instance.

Starting here, the document has to be reviewed...

Creating our own partition

At the moment, we have created a server which is not totally empty : one partition is created by default, the system partition. We won't use it for our tests, so we will need to create our own partition to play with. Let's call it 'o=sevenseas' (o stands for organization)

The setUp() method will be completed with all the needed instruction to create a new partition

import java.io.File;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Set;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.BasicAttribute;
import javax.naming.directory.BasicAttributes;
import javax.naming.directory.DirContext;

import org.apache.directory.server.core.configuration.MutablePartitionConfiguration;
import org.apache.directory.server.unit.AbstractServerTest;
...

    /**
     * Initialize the server.
     */
    public void setUp() throws Exception
    {
        // Add partition 'sevenSeas'
        MutablePartitionConfiguration pcfg = new MutablePartitionConfiguration();
        pcfg.setName( "sevenSeas" );
        pcfg.setSuffix( "o=sevenseas" );

        // Create some indices
        Set<String> indexedAttrs = new HashSet<String>();
        indexedAttrs.add( "objectClass" );
        indexedAttrs.add( "o" );
        pcfg.setIndexedAttributes( indexedAttrs );

        // Create a first entry associated to the partition
        Attributes attrs = new BasicAttributes( true );

        // First, the objectClass attribute
        Attribute attr = new BasicAttribute( "objectClass" );
        attr.add( "top" );
        attr.add( "organization" );
        attrs.put( attr );

        // The the 'Organization' attribute
        attr = new BasicAttribute( "o" );
        attr.add( "sevenseas" );
        attrs.put( attr );

        // Associate this entry to the partition
        pcfg.setContextEntry( attrs );

        // As we can create more than one partition, we must store
        // each created partition in a Set before initialization
        Set<MutablePartitionConfiguration> pcfgs = new HashSet<MutablePartitionConfiguration>();
        pcfgs.add( pcfg );

        configuration.setContextPartitionConfigurations( pcfgs );

        // Create a working directory
        File workingDirectory = new File( "server-work" );
        configuration.setWorkingDirectory( workingDirectory );

        // Now, let's call the upper class which is responsible for the
        // partitions creation
        super.setUp();
   }

Ok, now the partition sevenseas should be created. How can we be sure of that ? let's write a test to replace the emptytest() method :

/**
     * Test that the partition has been correctly created
     */
    public void testPartition() throws NamingException
    {
        Hashtable<Object, Object> env = new Hashtable<Object, Object>( configuration.toJndiEnvironment() );

        // Create a new context pointing to the overseas partition
        env.put( Context.PROVIDER_URL, "o=sevenSeas" );
        env.put( Context.SECURITY_PRINCIPAL, "uid=admin,ou=system" );
        env.put( Context.SECURITY_CREDENTIALS, "secret" );
        env.put( Context.SECURITY_AUTHENTICATION, "simple" );
        env.put( Context.INITIAL_CONTEXT_FACTORY, "org.apache.directory.server.jndi.ServerContextFactory" );

        // Let's open a connection on this partition
        InitialContext initialContext = new InitialContext( env );

        // We should be able to read it
        DirContext appRoot = ( DirContext ) initialContext.lookup( "" );
        assertNotNull( appRoot );

        // Let's get the entry associated to the top level
        Attributes attributes = appRoot.getAttributes( "" );
        assertNotNull( attributes );
        assertEquals( "sevenseas", attributes.get( "o" ).get() );

        Attribute attribute = attributes.get( "objectClass" );
        assertNotNull( attribute );
        assertTrue( attribute.contains( "top" ) );
        assertTrue( attribute.contains( "organization" ) );
        // Ok, everything is fine
    }

The test should succeed. Is that all ? Well, almost. As you can see, a working space has been created ( "server-work", at the end of the setup). Do we have to take care of this working space? No. It has been cleaned by the super class !

So everything is fine, the partition is up and running, you are ready to add more tests.

One line in the logs is interesting :

3879 [main] INFO org.apache.directory.server.jndi.ServerContextFactory -
LDIF load directory not specified.  No LDIF files will be loaded.

The setup has tried to load an LDIF file to inject some data into the partition, but as we didn't specify any Ldif file to be loaded, nothing has been done. let's add some data !

Adding some data into the partition

The AbstractServerTest class provide a method called importLdif( InputStream in ). It allows you to inject some entries into the newly created partition. How d we use it?

First, we need to have a valid LDIF file containing your entries. We will create two branches in our sevenseas organization :

  • one for groups
  • one for people

Here is the ldif file we will create :

dn: ou=groups,o=sevenSeas
objectClass: organizationalUnit
objectClass: top
ou: groups
description: Contains entries which describe groups (crews, for instance)

dn: ou=people,o=sevenSeas
objectClass: organizationalUnit
objectClass: top
ou: people
description: Contains entries which describe persons (seamen)

Save it as a text file into a directory where we will be able to read it directly. But where?

We have created the test into a directory src/test/java/org/apache/directory/demo. This is the maven way to organized the sources, as seen before. Let's create another directory for the resources : src/test/resources/org/apache/directory/demo. Tis is the place where we will save the ldif file.

Now, to let the server know about the ldif file, just add this line after the call to the setup() method :

...
        // Now, let's call the upper class which is responsible for the
        // partitions creation
        super.setUp();

        // Load a demo ldif file
        importLdif( this.getClass().getResourceAsStream( "demo.ldif" ) );
    }

This is important to add the import after the setup : you can't import data while the partition has not been created ...

The getResourceAsStream call will automatically read the file from the resources directory, based on the current class package.

How can we be sure that the data has been imported ? Let's do a search request !

Cleanup the code

Before that, let's do some cleanup. The context creation is something we will have to do in each test. We should create a common method called every time to avoid duplicating this code. Let's create this method :

/**
     * Create a context pointing to a partition
     */
    private DirContext createContext( String partition ) throws NamingException
    {
        // Create a environment container
        Hashtable<Object, Object> env =
            new Hashtable<Object, Object>( configuration.toJndiEnvironment() );

        // Create a new context pointing to the partition
        env.put( Context.PROVIDER_URL, partition );
        env.put( Context.SECURITY_PRINCIPAL, "uid=admin,ou=system" );
        env.put( Context.SECURITY_CREDENTIALS, "secret" );
        env.put( Context.SECURITY_AUTHENTICATION, "simple" );
        env.put( Context.INITIAL_CONTEXT_FACTORY,
                    "org.apache.directory.server.jndi.ServerContextFactory" );

        // Let's open a connection on this partition
        InitialContext initialContext = new InitialContext( env );

        // We should be able to read it
        DirContext appRoot = ( DirContext ) initialContext.lookup( "" );
        assertNotNull( appRoot );

        return appRoot;
    }

This method is added into the body of the test class. This method is very simple and quite straightforward : we just create an initial context pointing to the requested partition, and return a directory context on this partition. It takes a parameter, the partition name.

Let's modify the test partition method :

/**
     * Test that the partition has been correctly created
     */
    public void testPartition() throws NamingException
    {
        DirContext appRoot = createContext( "o=sevenSeas" );

        // Let's get the entry associated to the top level
        Attributes attributes = appRoot.getAttributes( "" );
        ...

We just replaced the first lines by a call to the newly created createContext() method.

If you launch the unit test, it should still be ok.

Searching for entries

This is really simple. What we will do is to search for the imported entries. Let's go directly to the code. We will add a new unit test

/**
     * Test that the ldif data has correctly been imported
     */
    public void testImport() throws NamingException
    {
        // Searching for all
        Set<String> result = searchDNs( "(ObjectClass=*)", "o=sevenSeas", "",
                                          SearchControls.ONELEVEL_SCOPE );

        assertTrue( result.contains( "ou=groups,o=sevenSeas" ) );
        assertTrue( result.contains( "ou=people,o=sevenSeas" ) );
    }

Here, we are looking for all the entries starting at the top level of the partition, within the level. We should only get two entries.

It's not enough : the searchDNs() method does not exist. It is a private method we have created to avoid duplicating some code all over the unit tests. Here is its code :

/**
     * Performs a single level search from a root base and
     * returns the set of DNs found.
     */
    private Set<String> searchDNs( String filter, String partition, String base, int scope )
       throws NamingException
    {
        DirContext appRoot = createContext( partition );

        SearchControls controls = new SearchControls();
        controls.setSearchScope( scope );
        NamingEnumeration result = appRoot.search( base, filter, controls );

        // collect all results
        HashSet<String> entries = new HashSet<String>();

        while ( result.hasMore() )
        {
            SearchResult entry = ( SearchResult ) result.next();
            entries.add( entry.getName() );
        }

        return entries;
    }

As for the test partiton, we call the createContext() method, then we just do some JNDI magic :

  • creating a SearchControl,
  • setting the scope,
  • calling the serach
  • and gathering the returned DN if any

If the test is successful, you get the imported DNs !

Adding your own schema

Work in progress

This paragraph is totally outdated. It should be rewriten from scratch !

Ok, let's go deeper into the server configuration. Working with default schema is fine, but some point, you may want to use your own ObjectClasses and AttributeTypes. let's assume you have created them, and that you have been throw the process of generating the class files for it (this process is described in Custom Schema) into a new package (org.apache.directory.demo.schema) where all the generated files will be put.

You will just have to add those lines at the end of the setUp() method (just before the call to the super() method) :

...
import org.apache.directory.server.core.configuration.MutablePartitionConfiguration;
import org.apache.directory.server.core.schema.bootstrap.AbstractBootstrapSchema;
import org.apache.directory.server.unit.AbstractServerTest;

import org.apache.directory.demo.schema.DemoSchema;

...
        ...
        ///
        /// add the Demo schema
        ///
        Set<AbstractBootstrapSchema> schemas = configuration.getBootstrapSchemas();
        schemas.add( new DemoSchema() );

        configuration.setBootstrapSchemas(schemas);


        // Now, let's call the upper class which is responsible for the
        // partitions creation
        super.setUp();
    }

If we launch the test, nothing special will happen, except that the test will succeed. That's not very impressive...

Conclusion

Ok, this tutorial was a short one, but you get everything you need to play with Apache Directory Server as a Unit Test Engine for your Ldap application. Just create your own partition, define your schema, import your ldif file, and add all the tests you need. it's as simple as explained

If you have any problem, just feel free to post a mail to users@directory.apache.org, we will be there to help !

Resources

Powered by Atlassian Confluence (Version: 2.2.9 Build:#527 Sep 07, 2006) - Bug/feature request

Unsubscribe or edit your notifications preferences