ApacheDSを使ってLDAPアクセスをユニットテストする
今見ているコードがLDAPアクセスコードがあちこちに混ざっており、ユニットテストが非常にやりづらい。仕方ないのでJMockitを使って無理矢理ユニットテストを書いていたけど、書くのが結構大変なのとLDAPアクセス処理に変更が入った途端にテストコードが動かなくなるので、ApacheDSを使ってテスト用にEmbedded LDAP Serverを立てる方法を試してみた。
参考
- http://directory.apache.org/apacheds/1.5/42-using-apacheds-for-unit-tests.html:title=
- サンプルコードが書かれているのだがそのままでは動かず、ドキュメントが最新化されていないように見える。例えば、独自のパーティションを作成するコード例が古い。アノテーションベースになっておらず、恐らくAbstractServerTestを使った継承ベースでのユニットテストの時代のものか?
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. ...
気になるところ
- 単純なパターンなら問題なさそうだけど、スキーマの拡張をしている場合はどうすれば良いのかな?スキーマをロードさせる方法がある?
- 本番環境だとOpenLDAPを良く使うのですが、OpenLDAPのppolicyを使っている場合にLDAP Controlを使って制御が可能になるが、ApacheDSではテストできなくないか? 一応、ApacheDS v1.5 - Account and Password Policy Managementを見るとサポートしてそうだが...