ApacheDSを使ってLDAPアクセスをユニットテストする

今見ているコードがLDAPアクセスコードがあちこちに混ざっており、ユニットテストが非常にやりづらい。仕方ないのでJMockitを使って無理矢理ユニットテストを書いていたけど、書くのが結構大変なのとLDAPアクセス処理に変更が入った途端にテストコードが動かなくなるので、ApacheDSを使ってテスト用にEmbedded LDAP Serverを立てる方法を試してみた。

参考

1. 必要なライブラリの取得

ApacheDSでは、全部入りのJARとしてapacheds-all-*.jarが配布されているのでこれを使う。その他、依存ライブラリを合わせると必要なJARは以下の通り。SLF4Jの実装はお好みに合わせて変更すれば良いが、1.6系だとエラーがでるので1.5系を使うこと

  • apacheds-all-1.5.7.jar
  • commons-io-1.4.jar
  • slf4j-jdk14-1.5.10.jar
  • junit-4.10.jar

JARの取得は、Eclipse3.7を使っていればMavenが統合されているのでpom.xmlを書いた方が楽です。ただし注意点あり。本家のガイドにもpom.xmlの例があるが、あの通りに書くと多数のJARをクラスパスに追加してしまい起動時に複数のLDIFが読まれて以下のエラーになってしまう。

Multiple copies of resource named 'schema/ou=schema/cn=apachedns/ou=syntaxes.ldif' located on classpath at urls
    jar:file:/Users/wadahiro/.m2/repository/org/apache/directory/server/apacheds-all/1.5.7/apacheds-all-1.5.7.jar!/schema/ou%3dschema/cn%3dapachedns/ou%3dsyntaxes.ldif
    jar:file:/Users/wadahiro/.m2/repository/org/apache/directory/shared/shared-ldap-schema/0.9.19/shared-ldap-schema-0.9.19.jar!/schema/ou%3dschema/cn%3dapachedns/ou%3dsyntaxes.ldif

なので、以下のようにexclusionで余計なJARがクラスパスに追加されないように除外設定をしておく。なお、*指定ができるのはMaven3以降かららしい。後、apacheds-server-integ、apacheds-core-integは使わないので依存関係は無くても良い。

<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/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>ldap-test</groupId>
	<artifactId>ldap-test</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<dependencies>
		<dependency>
			<groupId>org.apache.directory.server</groupId>
			<artifactId>apacheds-all</artifactId>
			<version>1.5.7</version>
			<scope>test</scope>
			<exclusions>
				<exclusion>
					<groupId>*</groupId>
					<artifactId>*</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
		<dependency>
			<groupId>commons-io</groupId>
			<artifactId>commons-io</artifactId>
			<version>1.4</version>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.slf4j</groupId>
			<artifactId>slf4j-jdk14</artifactId>
			<version>1.5.10</version>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<version>4.10</version>
			<scope>test</scope>
		</dependency>
	</dependencies>
</project>

2. LDIFの作成

テストデータとしてLDAPに格納するためのLDIFを作成する。クラスパスから検索してロードされるため、src/test/resourcesあたりに配置しておく。

dn: dc=example,dc=org
objectclass: top
objectclass: domain
objectclass: extensibleObject
dc: example

dn: ou=groups,dc=example,dc=org
objectclass: top
objectclass: organizationalUnit
ou: groups

dn: ou=people,dc=example,dc=org
objectclass: top
objectclass: organizationalUnit
ou: people

dn: uid=sample001,ou=people,dc=example,dc=org
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: Sample001CN
sn: Sample001SN
uid: sample001
userPassword: password

dn: uid=sample002,ou=people,dc=example,dc=org
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: Sample002CN
sn: Sample002SN
uid: sample002
userPassword: password

3. テストコード

ApacheDSのアノテーションで起動するLDAPサーバの設定情報を定義する。ポイントは以下の通り。

@RunWith(FrameworkRunner.class)
@CreateLdapServer(transports = { @CreateTransport(protocol = "LDAP", port = 50389) })

JUnitの@RunWithを使いって、ApacheDSが用意してくれているJUnitRunnerのFrameworkRunner.classを指定する。これにより、ユニットテストの開始前に自動的にLDAPサーバを起動し、テスト完了時には自動的にLDAPサーバを停止してデータもクリアしてくれる。@CreateLdapServerでは起動するLDAPサーバのプロトコル、ポートを指定する。

@CreateDS(partitions = @CreatePartition(name = "example", suffix = "dc=example,dc=org"))

ApacheDSではデータを投入する前に事前にパーティションを作成する必要がある。パーティションは、ルートディレクトリとなるベースサフィックスごとに作成する。しかし、本家のガイドにはパーティションを作成する方法が書いていなくて、デフォルトパーティション(ou=system)しかないのであれば使い物にならね〜と悩んでいたが、ApacheDSのソースを読んでいると@CreateDSと@CreatePartitionを使えばパーティションの追加ができることが判明。今回の例だと1つなのでsuffixには"dc=example,dc=org"を設定する。

@ApplyLdifFiles("test-server.ldif")

@ApplyLdifFilesにより、テスト開始前にLDIFを投入することができる。「2. LDIFの作成」で作成したファイル名を指定する。なお、@ApplyLdifsを使えばLDIFファイルを使わずに、LDIFの内容をアノテーション内の値として埋め込むこともできる。

※2012/01/06 AbstractLdapTestUnitの継承について追記

public class SearchTests extends AbstractLdapTestUnit {

AbstractLdapTestUnitを継承する。せっかくアノテーションベースなのに継承必須ってどうなの?と思ったら、特定のフィールドが宣言されていれば良いだけのようだ。LDAPサーバを使うのであれば、以下のようにstaticフィールドを3つ宣言すれば継承しなくても良い。

public class SearchTests {
	public static DirectoryService service;
	public static boolean isRunInSuite;
	public static LdapServer ldapServer;

テストメソッド内は「javax.naming.*」を使った一般的なLDAPアクセスコードを書けばOKだが、バインドに使用する管理者ユーザは"uid=admin,ou=system"、パスワードは"secret"となる部分だけ注意。今回はサンプルなのでテストコードのみだが、実際はテスト対象のコード側にこの処理はあり、プロパティファイルなどで外部設定化するのが一般的。ユニットテスト実行時はテスト用に切り替える必要がある。

		env.put(Context.SECURITY_PRINCIPAL, "uid=admin,ou=system");
		env.put(Context.SECURITY_CREDENTIALS, "secret");

全体のコードは以下のようになります。

@RunWith(FrameworkRunner.class)
@CreateLdapServer(transports = { @CreateTransport(protocol = "LDAP", port = 50389) })
@CreateDS(partitions = @CreatePartition(name = "example", suffix = "dc=example,dc=org"))
@ApplyLdifFiles("test-server.ldif")
public class SearchTests extends AbstractLdapTestUnit {

	@Test
	public void testSearchAllAttrs() throws Exception {
		LdapContext ctx = (LdapContext) getContext().lookup(
				"ou=people,dc=example,dc=org");

		SearchControls controls = new SearchControls();
		controls.setSearchScope(SearchControls.ONELEVEL_SCOPE);
		controls.setReturningAttributes(new String[] { "+", "*" });

		NamingEnumeration<SearchResult> res = ctx.search("", "(ObjectClass=*)",
				controls);

		assertTrue(res.hasMore());

		while (res.hasMore()) {
			SearchResult result = (SearchResult) res.next();

			System.out.println("DN=" + result.getNameInNamespace());
			System.out.println("RDN=" + result.getName());
			System.out.println("-------------------------------");

			Attributes attributes = result.getAttributes();
			NamingEnumeration<? extends Attribute> all = attributes.getAll();
			while (all.hasMore()) {
				Attribute attr = all.next();
				System.out.println(attr.getID() + ": " + attr.get());
			}

			System.out.println("-------------------------------");
		}
	}

	private Context getContext() throws NamingException {
		Hashtable<String, String> env = new Hashtable<String, String>();

		env.put(Context.INITIAL_CONTEXT_FACTORY,
				"com.sun.jndi.ldap.LdapCtxFactory");
		env.put(Context.PROVIDER_URL, "ldap://localhost:50389");

		env.put(Context.SECURITY_PRINCIPAL, "uid=admin,ou=system");
		env.put(Context.SECURITY_CREDENTIALS, "secret");
		return new InitialDirContext(env);
	}
}

4. 実行

ユニットテストを実行するとずらずらとApacheDSのログがたくさん出る。以下のようにちゃんとテストコード内のLDAPサーチで結果がとれている。サーバ起動は5秒くらいで、スローテストにならないかちょっと心配。実行速度については今後実践投入してみて確認してみよう。

...
情報: Ldap service started.
Using partition factory JdbmPartitionFactory
DN=uid=sample001,ou=people,dc=example,dc=org
RDN=uid=sample001
-------------------------------
sn: Sample001SN
userpassword: [B@195b6aad
entryUUID: 993c9893-c95a-4e2b-b7db-5b44f15c328c
entryCSN: 20120105200816.581000Z#000000#000#000000
objectClass: organizationalPerson
uid: sample001
createTimestamp: 20120105110816Z
cn: Sample001CN
creatorsName: 0.9.2342.19200300.100.1.1=admin,2.5.4.11=system
-------------------------------
DN=uid=sample002,ou=people,dc=example,dc=org
RDN=uid=sample002
-------------------------------
sn: Sample002SN
userpassword: [B@40b890dc
entryUUID: bea8b7c4-ad82-411f-b7f4-12e263e6a76a
entryCSN: 20120105200816.591000Z#000000#000#000000
objectClass: organizationalPerson
uid: sample002
createTimestamp: 20120105110816Z
cn: Sample002CN
creatorsName: 0.9.2342.19200300.100.1.1=admin,2.5.4.11=system
-------------------------------
2012/01/05 20:08:16 org.apache.directory.server.ldap.LdapServer stop
情報: Unbind of an LDAP service (50389) is complete.
...

気になるところ