Seasar DI Container with AOP

S2では、コンテナを使った開発のテストを楽しくおこなえるようにテスティングフレームワークが組み込まれています。JUnitを拡張しています。主な機能は以下のとおりです。

  • テストメソッド(testXxx)ごとに自動的にS2Containerを作成します。
  • S2Containerに対するregister(),getComponent(),include()が用意されています。
  • include()するPATHがテストクラスと同じパッケージにある場合は、パッケージ部分のパスは省略できます。例えば、aaa.bbb.CccTestクラスがaaa/bbb/hoge.diconをinclude()する場合、include("hoge.dicon")と記述することができます。
  • TestCaseのstaticでもないfinalでもないフィールドがあり、その名前からアンダースコア(_)を除いた名前のコンポーネントがコンテナに存在すれば自動的にセットされます。
  • TestCaseのフィールドにインターフェース型の変数が定義されていればS2Containerから取り出して自動的にセットされます。
  • テストメソッドが終わると自動セットされた値は自動的にクリア(nullをセット)されます。
  • テストメソッド(testXxx)に対応するsetUpXxx(),tearDownXxx()を定義しておくと、setUp()の後、tearDown()の前に自動的に呼び出されます。個別のテストメソッドごとの初期化・終了処理を簡単に行えるようになります。
  • include("j2ee.dicon")をしておいて、テストメソッド名の最後にTxをつける(testXxxTx)と、テストメソッドの直前にトランザクションを開始し、テストメソッドの直後にトランザクションをロールバックするので、データベースに関するテストを行った場合のクリーンアップの処理が不要になります。
  • reload(DataSet)を使って、データの中身をプライマリーキーでリロードして新しいDataSetを取得できます。更新後の予想される結果をExcelで定義しておき、DataSet expected = readXls("予想される結果.xls");assertEquals(expected, reload(expected);のようにして簡単に更新のテストができます。
  • assertEquals()で予想されるDataSetの結果に対して、Map、MapのList、Bean、BeanのListと比較することができます。

Example

ここでは、トランザクションの自動制御をテストするRequiredInterceptorTestを見てみましょう。

TxBean.java

package test.org.seasar.extension.tx;

import javax.transaction.SystemException;

public interface TxBean {

    public boolean hasTransaction() throws SystemException;
}

TxBeanImpl.java

package test.org.seasar.extension.tx;

import javax.transaction.Status;
import javax.transaction.SystemException;
import javax.transaction.TransactionManager;

public class TxBeanImpl implements TxBean {

    private TransactionManager tm_;

    public TxBeanImpl(TransactionManager tm) {
        tm_ = tm;
    }

    public boolean hasTransaction() throws SystemException {
        System.out.println(tm_.getTransaction());
        return tm_.getStatus() != Status.STATUS_NO_TRANSACTION;
    }
}

RequiredInterceptorTest.dicon

<components>
<component class="org.seasar.extension.jta.TransactionManagerImpl"/>
<component name="requiredTx"
class="org.seasar.extension.tx.RequiredInterceptor"/>
<component class="test.org.seasar.extension.tx.TxBeanImpl">
<aspect>requiredTx</aspect>
</component>
</components>

RequiredInterceptorTest.java

package test.org.seasar.extension.tx;

import javax.transaction.Status;
import javax.transaction.TransactionManager;

import junit.framework.Test;
import junit.framework.TestSuite;

import org.seasar.extension.unit.S2TestCase;

public class RequiredInterceptorTest extends S2TestCase {

    private static final String PATH =
        "RequiredInterceptorTest.dicon";
    private TxBean txBean_;
    private TransactionManager tm_;

    public RequiredInterceptorTest(String name) {
        super(name);
    }

    public void testInvoke() throws Exception {
        assertEquals("1", true, txBean_.hasTransaction());
        assertEquals("2", Status.STATUS_NO_TRANSACTION, tm_.getStatus());
    }

    public void testInvoke2() throws Exception {
        tm_.begin();
        assertEquals("1", true, txBean_.hasTransaction());
        assertEquals("2", Status.STATUS_ACTIVE, tm_.getStatus());
        tm_.commit();
    }

    protected void setUp() throws Exception {
        include(PATH);
    }

    protected void tearDown() throws Exception {
    }

    public static Test suite() {
        return new TestSuite(RequiredInterceptorTest.class);
    }

    public static void main(String[] args) {
        junit.textui.TestRunner.main(
            new String[] { RequiredInterceptorTest.class.getName()});
    }
}
コンポーネントの組み立てやコンポーネントの取り出しといった作業はS2TestCaseが自動的にやってくれて、各テストメソッドは本来必要なことに集中できることが分かっていただけたと思います。

データベースに対するテスト

S2では、データベースに対するテストも簡単に行えるような仕組みを用意しています。それではさっそく例を見てみましょう。SQL文を発行するためのフレームワークとしてここでは最も単純なS2JDBCを使います。

Select文に対するテスト

今回は、従業員を従業員番号で検索するDAOをサンプルにします。シナリオとして従業員番号9900で検索をかけると、従業員番号9900の従業員テーブルと部署番号99の部署テーブルをジョインして返す想定とします。このケースをテストするためには、検索のための従業員テーブルと部署テーブルのデータと検索した結果を検証するためのデータが必要です。データはExcelで用意します。シート名がテーブル名で、シートの第1行にカラム名を2行目以降にデータを書き込みます。1から手でデータを作成してもいいのですが、ここでは既存のテーブルのデータを利用してテストデータを作成します。セットアップを参照してHSQLDBを起動しておきます。データベースの内容をExcelに書き出すDb2Excelが用意されているのでそれを使います。

test/examples/unit/Db2Excel.dicon

<components>
<include path="j2ee.dicon"/>
<component class="org.seasar.extension.dataset.impl.SqlReader">
<initMethod>#self.addTable("emp", "empno = 7788")</initMethod>
<initMethod>#self.addTable("dept", "deptno = 20")</initMethod>
</component>
<component class="org.seasar.extension.dataset.impl.XlsWriter"
instance="prototype">
<arg>"../src/test/examples/unit/getEmployeePrepare.xls"</arg>
</component>
</components>

データベースの内容をDataSetに読み込んでくれるのがSqlReaderです。addTable()の最初の引数はテーブル名(シート名)です。2番目の引数は条件になります。

DataSetをExcelに書き出してくれるのがXlsWriterです。コンストラクタでファイルのパスを指定します。パスはEclipseのデフォルト出力フォルダが起点になります。

test.examples.unit.Db2Excel

package test.examples.unit;

import org.seasar.extension.dataset.impl.SqlReader;
import org.seasar.extension.dataset.impl.XlsWriter;
import org.seasar.framework.container.S2Container;
import org.seasar.framework.container.factory.S2ContainerFactory;

public class Db2Excel {

    private static final String PATH =
        "test/examples/unit/Db2Excel.dicon";

    public static void main(String[] args) {
        S2Container container = S2ContainerFactory.create(PATH);
        container.init();
        try {
            SqlReader reader = (SqlReader)
                container.getComponent(SqlReader.class);
            XlsWriter writer = (XlsWriter)
                container.getComponent(XlsWriter.class);
            writer.write(reader.read());
        } finally {
            container.destroy();
        }
    }
}

S2ContainerからSqlReaderを取り出しread()、XlsWriterを取り出しwrite()するだけで、データベースの内容をExcelに書き出すことができます。なんかダイコン時代のプログラムって感じですね(笑)。Eclipseでtest.examples.unitのパッケージを最新表示させると、getEmployeePrepare.xlsが作成されていることが確認できると思います。getEmployeePrepare.xlsを右クリックしてアプリケーションから開く->システムエディタを選ぶとExcelが起動します。単にダブルクリックするとEclipse上にExcelが表示されてかなり使いにくいので気をつけてください。empシートのEMPNOを9900、ENAMEをSCOTT2に変更します。続いてdeptシートのDEPTNOを99、DNAMEをRESEARCH2に変更します。これで検索用の元データは用意できました。Excelで保存を選び終了させます。つぎにgetEmployeePrepare.xlsを右クリックして最新表示を選びます。これをやることで、Eclipseが変更があったことを認識し、デフォルトの出力フォルダにブックをコピーしてくれます。

次に結果を検証するためのデータを用意します。Db2Excel.diconを次のように書き換えます。

test/examples/unit/Db2Excel.dicon

<components>
<include path="j2ee.dicon"/>
<component class="org.seasar.extension.dataset.impl.SqlReader">
<initMethod>
#self.addSql("SELECT e.empno, e.ename, e.deptno, d.dname
FROM emp e, dept d WHERE empno = 7788 AND e.deptno = d.deptno", "emp") </initMethod>
</component>
<component class="org.seasar.extension.dataset.impl.XlsWriter"
instance="prototype">
<arg>"../src/test/examples/unit/getEmployeeResult.xls"</arg>
</component>
</components>

先ほどと同様な手順でgetEmployeeResult.xlsのempシートのEMPNOを9900、ENAMEをSCOTT2、DEPTNOを99、DNAMEをRESEARCH2に書き換えて保存します。直接作ったほうが早かった気がしますが、SqlReader.addSql()で任意のSelect文を実行できるんだよというデモということで(^^;)。これでテスト用のデータがそろいました。いよいよテストに取り掛かります。

EmployeeDao.dicon

<components>
<include path="j2ee.dicon"/>
<component class="examples.unit.EmployeeDaoImpl">
<property name="getEmployeeHandler">
<component class="org.seasar.extension.jdbc.impl.BasicSelectHandler">
<property name="sql">
"SELECT e.empno, e.ename, e.deptno, d.dname FROM emp e, dept d
WHERE e.empno = ? AND e.deptno = d.deptno"
</property>
<property name="resultSetHandler">
<component class="org.seasar.extension.jdbc.impl.BeanResultSetHandler">
<arg>@examples.unit.Employee@class</arg>
</component>
</property>
</component>
</property>
</component>
</components>

test.examples.unit.EmployeeDaoImplTest

package test.examples.unit;

import org.seasar.extension.dataset.DataSet;
import org.seasar.extension.unit.S2TestCase;

import examples.unit.Employee;
import examples.unit.EmployeeDao;

public class EmployeeDaoImplTest extends S2TestCase {

    private EmployeeDao dao_;

    public EmployeeDaoImplTest(String arg0) {
        super(arg0);
    }

    public void setUp() {

        include("examples/unit/EmployeeDao.dicon");
    }

    public void testGetEmployeeTx() throws Exception {
        readXlsWriteDb("getEmployeePrepare.xls");
        Employee emp = dao_.getEmployee(9900);
        DataSet expected = readXls("getEmployeeResult.xls");
        assertEquals("1", expected, emp);
    }

    public static void main(String[] args) {
        junit.textui.TestRunner.run(EmployeeDaoImplTest.class);
    }
}

setUp()がapp.diconの役割を担います。testGetEmployeeTxのようにテストメソッド名の最後にTxをつけるとテストメソッドを開始する直前にトランザクションを開始し、テストメソッドが終了した直後にトランザクションがロールバックされます。テストのためにデータベースに格納したデータもすべてロールバックしてもとに戻るためデータのクリーンアップを考える必要がなくなります。assertEquals()でDataSetとMap、MapのList、Bean、BeanのListと比較できるのですっきりしたテストコードになります。

readXlsWriteDb()、readXlsAllReplaceDb()で、テストのために用意したデータをデータベースに格納します。Excelのファイルがテストクラスと同じパッケージにある場合は、パッケージのパスを省略できます。readXlsWriteDb()、readXlsAllReplaceDb()はテスト後にロールバックしてデータが元に戻るようにtestXxxTx()の最初に実行してください。これらのメソッドは、シートの定義の逆順に削除した後にデータを挿入します。readXlsAllReplaceDb()を使う場合、外部キー制約に引っかからないように、データのないシートを用意する必要がある場合があります。例えば、テーブルAの外部キーでテーブルBを参照している場合、テーブルAのデータしか使わない場合でも、テーブルB用にシート名だけのシートを用意する必要があります。シートの定義順は、テーブルA、テーブルBの順になります。

インターフェース型でインスタンス変数(dao_)を宣言しておけば、テストメソッドを開始する前にS2Containerから取り出されて自動的に設定されます。Daoを呼び出して取得したデータは、Excelで用意したデータと結果を比較するためにDataTableに変換します。DataTable.setupColumns(Class)でJavaBeansのメタ情報よりカラムのデータを構築します。後は、DataTable.copyFrom()でDaoを呼び出した結果をDataTableにコピーします。copyFromの引数には、JavaBeans,Map,JavaBeansやMapを要素に持つListを指定することができます。

readXls()で結果検証用のExcelデータを読み込み、Daoの結果と比較します。S2Unitを使えば、楽しく・効率良くテストを行えるということを分かっていただけたと思います。