読者です 読者をやめる 読者になる 読者になる

Septeni Engineer's Blog

セプテーニエンジニアが綴る技術ブログ

Seasar2でのDaoテストコードをH2DB化

t_konoです。

大分、間が空いての投稿になります。
今回もマニアックなネタで需要がなさそうですが気にせず載せますよ。

弊社では現在あるプロジェクトでフレームワークとしてSeasar2+SAStrutsを使っています。
そして既にプロジェクトが開始してから数ヶ月経っていたのですが、諸事情によりテストコードの整備がほとんどされてなく、レガシーコード化していました。

そこでテストコードを構造化したり、パラメータ化テストやMockの導入などを進めて言ったのですが、頭を悩ませたのがDaoの既存のテストコードでした。
データベース(MySQL)に入れたデータを使って行われているので、そもそもMySQLにアクセス出来なかったり、そのデータが入ってなかったりするとテストが通りません。
レガシーコード改善ガイドのマイケルにも怒られます(;´∀`)

いや、別にテスト実行する時は必ずMySQLにアクセス出来る状態にしてテスト実行することというルールでも設ければいいんでしょうけど、Jenkinsでオートテストする場合にテスト用のDBを用意しないといけなくなりますし、とりあえずDBサーバがない状態でテストが動かないということは無しにする方向にしました。
Seasarが元々用意しているDaoのテストコードの仕組み(Excelデータとの突き合わせ)は、既存のテストコードとはマッチしていないことと、極力標準的なテストコードにしたいことから、利用しないことにしています。
まあ、利用してもいいんですけど、なんか混ざってめちゃくちゃになるのが嫌なんで。

MockもSeasarの推奨している(?)MockInterceptorやEasyMockではなくMockito使ってますし。
※ jmockitoに移行しようかと思ってますが。

そんな理由で、以下を実現するために手を出してみました。
1.テストコードで利用しているDBをMySQLからH2DBのMySQLモードに変更
2.テストデータは都度必要なデータのみCSVから作成
3.S2DaoTestCaseは利用せず、JdbcManagerをリフレクションで差し替え

ではやっていきます。
まず、H2DBへアクセスするためのdiconファイルを用意します。

/project/src/main/resources/junit-jdbc.dicon

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE components PUBLIC "-//SEASAR//DTD S2Container 2.4//EN"
  "http://www.seasar.org/dtd/components24.dtd">
<components namespace="jdbc">
  <include path="jta.dicon"/>

  <!-- for H2  -->
  <component name="xaDataSource"
    class="org.seasar.extension.dbcp.impl.XADataSourceImpl">
    <property name="driverClassName">
      "org.h2.Driver"
    </property>
    <property name="URL">
      "jdbc:h2:mem:test;MODE=MYSQL;DB_CLOSE_DELAY=-1"
    </property>
    <property name="user">"sa"</property>
    <property name="password">""</property>
    <destroyMethod>
      @org.seasar.framework.util.DriverManagerUtil@deregisterAllDrivers()
    </destroyMethod>
  </component>

  <component name="connectionPool"
    class="org.seasar.extension.dbcp.impl.ConnectionPoolImpl">
    <property name="timeout">600</property>
    <property name="maxPoolSize">10</property>
    <property name="allowLocalTx">true</property>
    <destroyMethod name="close"/>
  </component>

  <component name="DataSource"
    class="org.seasar.extension.dbcp.impl.DataSourceImpl"
  />

</components>


元々サンプルとして用意されているjdbc.diconでH2DBの定義だけ残して
URLのディレクティブだけ書き換えています。

次にdialectの設定のためのdiconファイルを用意します。

/project/src/main/resources/junit-s2jdbc.dicon

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE components PUBLIC "-//SEASAR//DTD S2Container 2.4//EN"
	"http://www.seasar.org/dtd/components24.dtd">
<components>
	<include path="junit-jdbc.dicon"/>
	<include path="s2jdbc-internal.dicon"/>
	<component name="h2JdbcManager" class="org.seasar.extension.jdbc.manager.JdbcManagerImpl">
		<property name="maxRows">0</property>
		<property name="fetchSize">0</property>
		<property name="queryTimeout">0</property>
		<property name="dialect">h2Dialect</property>
	</component>
</components>


H2DBに接続するためのh2JdbcManagerを定義しています。
このファイルはテストコードでしか利用しないため、app.diconにincludeの定義は記述しません。

これで設定ファイルの準備は終わりです。

次にS2Containerの用意とjdbcManagerの差し替えを行います。
Daoのテストコード全てで使うので継承される前提で親クラスを用意しました。
名前は適当にDaoTestBaseです。

public class DaoTestBase {

    /**
     * Daoテストコードの事前準備(DaoのJdbcManagerの置き換えを行う)
     * 
     * @param DaoInstance
     *            対象のDaoのインスタンスを指定
     * @throws Exception
     */
    protected static void beforeClass(Object DaoInstance) throws Exception {
        
        // S2Containerを初期化
        S2Container s2Conteainer = S2ContainerFactory.create("junit-s2jdbc.dicon");
        s2Conteainer.init();

        // JDBCManagerを取得
        jdbcManager = (JdbcManager) s2Conteainer.getComponent("h2JdbcManager");

        Field refJdbcManager = S2AbstractService.class.getDeclaredField("jdbcManager");
        refJdbcManager.setAccessible(true);

        // DaoのJdbcManagerを置換
        refJdbcManager.set(DaoInstance, jdbcManager);
    }
    
    /**
     * テーブルを作成
     * 
     * @param jdbcManager
     * @param tableName
     * @throws IOException
     */
    public static void create(String tableName) throws IOException {
        // sqlファイルを構成
        File createSqlFile = new File(rootPath + FS + [マイグレート用ファイルのパス] + FS + tableName + ".sql");

        String sql = readQuery(createSqlFile);

        // SQL実行
        jdbcManager.callBySql(sql).execute();
    }
    
     /**
     * テーブルを削除
     * 
     * @param tableName
     * @throws IOException
     */
    public static void drop(String tableName) throws IOException {
            // sqlファイルを構成
        File dropSqlFile = new File(System.getProperty("user.dir") + FS + [マイグレート用ファイルのパス] + FS + tableName + ".sql");

        String sql = readQuery(dropSqlFile);

        // SQL実行
        jdbcManager.callBySql(sql).execute();
    }
}

※ readQueryはSQLファイルを読み込むだけのメソッドなので省略しておきます。

実際のクラスはもう少しブラッシュアップしていますが、大体こんな感じです。
このクラスを継承してDaoのテストコードを記述します。
その際、@BeforeClassアノテーションを記述したメソッド内で親クラスのbeforeClassを実行すれば、DaoのjdbcManagerが差し替わります。

テストコード単位でDBを初期化するので利用するテーブルをcreateメソッドで作成しておきます。
以後はそのDao経由でデータのinsertを行ってから、テストコードを記述していけばH2DBを利用したテストコードとなります。

また、複数のテストコードを実行することを想定して、テストデータは都度破棄することにします。
そのためのメソッドとしてdropメソッドを用意しておきます。
drop用のSQLはmigrate用ファイルを流用します。
@AfterClassや@Afterを記述したメソッド内で呼ぶことでテーブルをdropします。

こんな流れです。

jdbcManager差し替え

テーブル作成

テスト用データ登録

テストコード実行

テーブル削除

あんまりリフレクション使ってスコープいじったりとかしちゃいけないのですが、テストコードでしか実行されずプロダクションコードに影響与えないので、許容してしまってます。
S2DaoTestCaseを継承すれば、リフレクション使わなくてもいいのですが、極力Seasarのテスト用クラスは利用しない方針なので致し方なしということで。

注意しなければいけないことは、H2DBをMySQLモードで実行しているは言え、MySQL完全互換ではないので、お互いのDBの差異は把握しておかないとハマることがあります。
例えば、異なるテーブルで同じIndex名が利用出来なかったり等。

これでDB環境に依存しないでテストが可能となりました。