【落とし穴】JUnitテストでJNDIのResource検索を行う方法

zuka

こんにちは。zuka(@beginaid)です。

この記事では,JUnitテストにおいてJNDIのResource検索を行う方法をお伝えしていきます。

なお,本記事では分かりやすさを優先するため,用語を正確に使わない部分や理解が曖昧な部分を残すことがあります。予めご了承ください。間違いがございましたら,お問い合わせフォームまたは最下部コメント欄よりご指摘いただけますと助かります。

目次

環境

  • Java: 11.0.10
  • Apache Tomcat: 9.0.44
  • OpenJDK: 15.0.2
  • JUnit: 5.7.1
  • Eclipse: 4.19.0
  • MySQL: 8.0.24
  • OS: Windows10

背景

Javaを使ったWebアプリケーションにおいて,データベースと接続したい場面があります。従来は,データベースと接続する.javaファイル内に接続情報を生で書き込んでいました。しかし,この方法では接続情報が変更された際の書き換えコストが高く,修正漏れのリスクも考えられます。

そこで,現在ではDataSourceと呼ばれるデータベースとの接続を一元管理する仕組みがデファクトスタンダードとなっています。複数のデータベースに対してJNDI(Java Naming and Directory Interface)を利用して名前を付けて登録することで,オブジェクトとしてデータベースを取得することができます。

以下ではMySQLを利用する一例を示します。

// 従来の方法
Connection con = DriverManager.getConnection("jdbc:mysql://localhost:3306/[データベース名]", "[ユーザ名]", "[パスワード]");	

// DataSourceを利用する方法
String localName = "java:comp/env/jdbc/[データベース名]";
Context context = new InitialContext();
DataSource ds = (DataSource) context.lookup(localName);
Connection con = ds.getConnection();

DataSourceではコネクションプールと呼ばれる技術を利用することができます。具体的には,アプリケーションサーバの起動時にデータベースと複数のコネクションを確立しておくことで,SQLのリクエストを取得した際にすぐコネクションを渡すことができます。SQLのリクエストを取得するたびにコネクションを確立する負荷を分散するために利用される技術です。

JUnitテストとの相性

通常,データベースとの接続情報はcontext.xmlに書きます。しかし,JUnitではサーブレットコンテナ(tomcat)を起動させないらしく(要出典),単純にJUnitテストを行うだけではcontext.xmlを参照せず,NoInitialContextExceptionを引き起こしてしまいます。ここが大きな落とし穴になっています。

そこで,JUnitテストのセットアップでcontext.xmlに書き込むべき内容について設定してあげる必要があります。

DataSourceの初期設定

具体的には,以下のような方法でDataSourceの初期設定を行ってあげましょう。ただし,ここではUserDAOクラスのテストを行うものとしています。

全てのテストの前に1回だけ実行される@BeforeAllアノテーションを駆使します。@BeforeEachアノテーションを利用してしまうと,Contextの重複定義として怒られてしまいます。

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;

import org.junit.jupiter.api.BeforeAll;

import com.mysql.cj.jdbc.MysqlDataSource;

class UserDAOTest {

	// テストが行われる前に1回だけ行われる処理を記述する
	@BeforeAll
	static void setUpContext() throws Exception {
		try {
			System.setProperty(Context.INITIAL_CONTEXT_FACTORY,
					"org.apache.naming.java.javaURLContextFactory");
			System.setProperty(Context.URL_PKG_PREFIXES,
					"org.apache.naming");
			InitialContext ic = new InitialContext();

			ic.createSubcontext("java:");
			ic.createSubcontext("java:comp");
			ic.createSubcontext("java:comp/env");
			ic.createSubcontext("java:comp/env/jdbc");

			MysqlDataSource ds = new MysqlDataSource();
			ds.setUser("[ユーザ名]");
			ds.setPassword("[パスワード]");
			ds.setDatabaseName("[データベース名]");
			ds.setServerName("localhost");
			ds.setPortNumber(3306);

			ic.bind("java:comp/env/jdbc/[リソース名]", ds);
		} catch (NamingException e) {
			e.printStackTrace();
		}
	}
}

あとは,JUnitテストの定石通り@testアノテーションを付与して単体テストを記述して実行すればOKです。ちなみに,javaURLContextFactorynaming packageはtomcatで使われているものらしいです。一次情報が見つかり次第追記しますが,ここはトップダウンに受け入れるしかなさそうです。

さらに,自分の環境ではtomcat-juli.jarが見つからないと怒られてしまったため,eclipseのビルド・パスに追加することでエラーを解消しました。

管理人が引っかかった落とし穴

なんでJUnitテストと通常の実行では挙動が異なるの?

JUnitテストではどうやら独自のコンテナを作成して単体テストを実行しているらしいです。一次情報が見つかり次第追記します。

データベースと接続できないということはmysql-connectorの配置場所が悪いのでは?

いえ,恐らくmysql-connectorの配置場所が悪ければClassNotFoundExceptionになるはずです。

ググったらC:\pleiades\tomcat\9\conf下のserver.xmlをいじれって出てきたけど?

たしかに,すべてのすべてのプロジェクトに設定を反映させたいのであればtomcatのserver.xmlを設定してあげる必要があります。しかし,特定のプロジェクトだけに設定を反映させたいのであればMETA-INF下のcontext.xmlやWEB-INF下のweb.xmlに設定を記述してあげればOKのようです。

通常実行時のcontext.xmlとweb.xmlのテンプレを教えて!

Apache Tomcat 9の公式ドキュメント「JNDI Resources」と「JDBC DataSources」を参考にしましょう。

特に,MySQL 8を利用する場合はMySQL 5とは異なりドライバのクラス名にはcom.mysql.cj.jdbc.Driverを利用したり,urlにタイムゾーンの設定が必要だったりするため,注意が必要です。

ちなみに,自分の環境ではserverTimezoneにJSTを指定したら「そんなserverTimezoneはありません」と怒られてしまいました。Asia/Tokyoとして設定するのが吉です。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE Context>
<Context reloadable="true">
    <Resource name="jdbc/[付けたいリソース名]"
              auth="Container"
              type="javax.sql.DataSource"
              driverClassName="com.mysql.cj.jdbc.Driver"
              url="jdbc:mysql://localhost:3306/[サーバ名]?serverTimezone=Asia/Tokyo&characterEncoding=UTF-8"
              username="[ユーザ名]"
              password="[パスワード]"
              maxTotal="100"
              maxIdle="30"
              maxWaitMillis="10000"
              validationQuery="SELECT 0"
              removeAbandonedOnBorrow="true"
              removeAbandonedTimeout="60"
              />
</Context>
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" id="WebApp_ID" version="4.0">
  <display-name>Ecsite</display-name>
  <welcome-file-list>
    <welcome-file>index.html</welcome-file>
    <welcome-file>index.htm</welcome-file>
    <welcome-file>index.jsp</welcome-file>
    <welcome-file>default.html</welcome-file>
    <welcome-file>default.htm</welcome-file>
    <welcome-file>default.jsp</welcome-file>
  </welcome-file-list>
    <resource-ref>
        <res-ref-name>jdbc/[付けたいリソース名(バグを防ぐためcontext.xmlと一致させる)]</res-ref-name>
        <res-type>javax.sql.DataSource</res-type>
        <res-auth>Container</res-auth>
    </resource-ref>
</web-app>
よかったらシェアしてね!

コメント

コメントする

目次
閉じる