Mocking out LDAP/JNDI in unit tests

When unit testing a class that queries an LDAP server using Java's JNDI API I needed to replace the actual remote LDAP server with a mock LDAP access layer so that the unit test (remember, this is not an integration test) doesn't depend on any external SW/HW. Few hours of googling haven't yielded any suitable mock implementation and so I had to create my own one. It turned out to be an easy task after all. I hope it could be useful for you too.

To create a test implementation of LDAP/JNDI you need to:
  1. Hook you mock JNDI implementation into the JVM and make sure that you use it
  2. Actually implement the JNDI layer by implementing/mocking few (3) JNDI classes, mostly from the javax.naming.directory package
  3. Configure your mock implementation to return the data expected

1. Configuring the JVM to use the test JNDI implementation

The best way to "inject" your test LDAP/JNDI implementation depends on the way your code is accessing it. There are basically two options:
  1. You specify explicitely the implementation to use via the parameter  INITIAL_CONTEXT_FACTORY
  2. You use the default implementation for the JVM
Let's see an example:


 new javax.naming.directory.InitialDirContext(new Hashtable(){{
  put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
  put(Context.PROVIDER_URL, "ldap://ldap.example.com:389");
 }});


The javax.naming.directory.InitialDirContext will delegate most operations to the actual implementation, which is provided either by the requested initial context factory if the line #2 is included or based on the JVM's configuration - see  NamingManager.getInitialContext(..). Therefore:
  1. If your code specifies explicitely the initial context factory, configure it to use your test initial context factory implementation, i.e. you modify the code to something like put(Context.INITIAL_CONTEXT_FACTORY, "your.own.MockInitialDirContextFactory") (you have that configurable, right?)
  2. If your code relies on the JVM's configuration to provide the proper implementation, configure it with a custom InitialContextFactoryBuilder, which will return your test initial context factory implementation. I won't go into the details here, you can see an example in the Spring's mock jndi SimpleNamingContextBuilder [source] (it mocks unfortunately only javax.naming, not the javax.naming.directory we need for LDAP)

2. Implementing/mocking JNDI classes

The test LDAP over JNDI implementation is quite simple. We need:
  1. The InitialContextFactory for creating our test contexts, as described above
  2. The test DirContext implementation itself, which we will mock using Mockito (the interface has many methods to implement while my code is using only one of them)
  3. And a NamingEnumeration implementation for returning search results from the mock DirContext's search method
The test initial context factory is very simple:


public class MockInitialDirContextFactory implements InitialContextFactory {

private static DirContext mockContext = null;

/** Returns the last DirContext (which is a Mockito mock) retrieved from this factory. */ public static DirContext getLatestMockContext() { return mockContext; }

public Context getInitialContext(Hashtable environment) throws NamingException { synchronized(MockInitialDirContextFactory.class) { mockContext = (DirContext) Mockito.mock(DirContext.class); } return mockContext; } }


We store the latest DirContext mock (the class under test only creates one so this is enough) so that we can tell it what calls to expect and what to return (i.e. to do some "stubbing").

We also need an implementation of the NamingEnumeration, which is returned by the various search methods. Because we actually do not use it we could also mock it with Mockito (simple Mockito.mock(NamingEnumeration.class) would be enough to replace all the lines below) but I've decided to create a real implementation so that in more involved tests it could be extended to actually be able of holding and returning some fake LDAP search data.

In this case the NamingEnumeration should hold instances of the conrete class SearchResult with the actual LDAP data in its field of the type Attributes, for which we can use the concrete BasicAttributes implementation provided by the JVM. But for now let's just return an empty enumeration.



public class MockNamingEnumeration/*<SearchResult>*/ implements NamingEnumeration {

public void close() throws NamingException { }

public boolean hasMore() throws NamingException { return hasMoreElements(); }

public Object next() throws NamingException { return nextElement(); }

public boolean hasMoreElements() { return false; }

public Object nextElement() { return null; } }


As you can see, this implementation will behave as if the search matched no records.

3. Using the test LDAP/JNDI implementation

The last piece is the actual JUnit test of a hypothetical TestedLdapReader class, which searches an LDAP server:


public class MyMockLdapTest extends TestCase {
	private TestedLdapReader ldapReader;
        ...
	protected void setUp() throws Exception {
		super.setUp();
		ldapReader = new TestedLdapReader();
		ldapReader.setInitialContextFactory(
			MockInitialDirContextFactory.class.getName());
		ldapReader.setLdapUrl("ldap://thisIsIgnoredInTests");
	}

public void testLdapSearch() throws Exception { ldapReader.initLdap(); // obtains an InitialDirContext... final DirContext mockContext = MockInitialDirContextFactory.getLatestMockContext(); //Stub the public NamingEnumeration search(String name, String filter, SearchControls cons) Mockito.when( mockContext.search( (String) Mockito.eq("ou=bluepages,o=ibm.com") , Mockito.anyString() , (SearchControls) Mockito.any(SearchControls.class))) // a custom 'answer', which records the queries issued .thenAnswer(new Answer() { public Object answer(InvocationOnMock invocation) throws Throwable { LOG.debug("LDAP query:" + invocation.getArguments()[1] ); return new MockNamingEnumeration(); } });

try { ldapReader.searchLdap(); } catch (Exception e) { LOG.warn("exception during execution", e); }

// Uncomment to find out the methods called on the context: // Mockito.verifyNoMoreInteractions(new Object[]{mockContext}); }


Let's summarize what we do here:
  • #07,08: We tell the class under test to use our test JNDI implementation
  • #13: It's assumed that this call instantiates an InitialDirContext supplying it the initial context factory class parameter set on the lines 07-08
  • #16-26:  We use Mockito to configure the mock DirContext to expect a search call for the context "ou=bluepages,o=ibm.com", any query string and any search controls and tell it to return an empty MockNamingEnumeration while also logging the actual LDAP query (the 2nd argument).
  • #29: The tested method is called
  • #35: If we are not sure what methods the tested method calls on the DirContext, we may uncomment this line to let Mockito check it (adding Mockito.verify(mockContext.<method name>(..)) prior to #35 for each method we know about already)

Summary

We've created a minimalist LDAP over JNDI implementation using partly real and partly mock objects. It could be easily extended to make it possible to configure the data returned from individual LDAP searches (currently we always return an empty collection) and thus test the behavior in reaction to different data sets. There is of course some space left for simplification.

Tags: testing java


Copyright © 2024 Jakub Holý
Powered by Cryogen
Theme by KingMob