[Java] ユニットテストでDataSourceを使う

2017年12月11日月曜日

Java JDBC MySQL

DataSourceとContext

Webアプリケーションのユニットテストで、JUnitとかの実行時にDBの接続先をどこから取得するかを考えなきゃいけない時がある。

たいていのWebアプリケーションではアプリケーションサーバで設定されているDataSourceをJNDIを使って取得しているからだ。

例えば、こんな感じ。
    DataSource ds = InitialContext.doLookup("jdbc/test");

    try (Connection conn = ds.getConnection();
         PreparedStatement ps = conn.prepareStatement("SELECT name FROM Test1");
         ResultSet rs = ps.executeQuery()) {
        while (rs.next()) {
            // 何か処理...
        }
    }

"jdbc/test"という名前でContextからDataSourceを取得して、そこからConnectionを取得している。
もちろん、さらにラッピングされて抽象化されているケースも多いと思うけど、実際の処理内容はこんなところだろう。

じゃあ、これらのメソッドをアプリケーションサーバを介さずに呼びたいときはどうすればいいだろうか。

ContextとInitialContextFactory

単に文字列でContextから必要なものを取得したいだけであれば、Contextを自前で提供してしまえばいい。
この場合はjavax.naming.Contextとjavax.naming.InitialContextFactoryを実装したクラスを用意する。

MyContextは単にマップの役割を果たす。

MyContext.java
public class MyContext implements Context {
    private final Hashtable<Object, Object> environment = new Hashtable<>();

    @Override
    public Object lookup(String name) throws NamingException {
        return environment.get(name);
    }

    @Override
    public void bind(String name, Object obj) throws NamingException {
        environment.put(name, obj);
    }

    // これ以外にもオーバーライドが必要なメソッドはたくさんあるけど、中身は最低限、lookupとbindを実装しておけば、OK。
    // なんで、Hashtableなんて使ってるかというと、Hashtable<?, ?> getEnvironment()という、抽象メソッドがあったから。
}

次にMyContextのファクトリクラスを作る。

MyContextFactory.java
public class MyContextFactory implements InitialContextFactory {
    private static final Context CONTEXT = new MyContext();

    @Override
    public Context getInitialContext(Hashtable<?, ?> environment) throws NamingException {
        return CONTEXT;
    }

}

Contextの初期化

さて、これら2つのクラスを作ったら、ユニットテストの初期化処理なりで、以下のようにContextを初期化して、"jdbc/test"とDataSourceを結び付けよう。
この場合はMySQLのドライバでlocalhostのtestdbに接続するように設定している。
    static void init() throws NamingException {
        // "local.MyContextFactory"の部分は実際のパッケージ名を指定しよう。
        System.setProperty(Context.INITIAL_CONTEXT_FACTORY, "local.MyContextFactory");
        System.setProperty(Context.URL_PKG_PREFIXES, "local");

        MysqlDataSource ds = new MysqlDataSource();
        ds.setURL("jdbc:mysql://localhost:3306/testdb");
        ds.setUser("testdb");
        ds.setPassword("testdb");

        Context ctx = new InitialContext();
        ctx.bind("jdbc/test", ds);
    }
これで、InitialContext.doLookup("jdbc/test") でDataSourceを取得できるようになる。

以下の部分はプログラムで設定しているけど
        System.setProperty(Context.INITIAL_CONTEXT_FACTORY, "local.MyContextFactory");
        System.setProperty(Context.URL_PKG_PREFIXES, "datasourcesample");

jndi.propertiesというファイルに設定して、クラスパスに保存することによって適用することもできる。

jndi.properties
java.naming.factory.initial=local.MyContextFactory
java.naming.factory.url.pkgs=local

ユニットテスト程度であれば、別ファイルにするよりもプログラムから呼び出したほうがわかりやすいかな。