Seasar DI Container with AOP

テストの概要

S2Containerではコンポーネント同士がインターフェイスのみで会話することにより依存関係をなくすということから、インターフェースの実装が出来上がってからテストを行うことになり開発のボトルネックが発生します。この問題を解決するために、Seasar2ではインターフェースのモックを簡単に作成できます。実装をモックオブジェクトに置き換えることが可能なため、出来上がった順にテストが出来るので、開発のボトルネックが発生しづらくなります。またコンテナを使った開発のテストを簡単に行う機能(S2Unit)、データベースに対するテストをExcelファイルで行う機能が提供されています。

S2テストリファレンス

モックを作成するための設定

   インターフェースのモックを設定するには、org.seasar.framework.aop.interceptors.MockInterceptorを使用します。MockInterceptorはS2AOPで提供されているInterceptorです。
   MockInterceptorの以下のメソッドを使用してインターフェイスのモックの設定、インターフェイスのメソッドの呼び出し、引数の確認が出来ます。

   public void setReturnValue(String methodName, Object returnValue)
        概要:インターフェイスのメソッドに戻り値をセットする場合に使用します。
        第1引数:インターフェイスのメソッド名を指定します。
        第2引数:第1引数で指定したメソッドの返す値を指定します。

   public void setReturnValue(Object returnValue)
        概要:インターフェイスのすべてのメソッドに戻り値をセットする場合に使用します。
        第1引数:返す値を指定します。この場合、どんなメソッドが呼ばれてもその値を返します。通常、1つのメソッドしか呼び出さないこと
        が分かっている場合に使います。

   public boolean isInvoked(String methodName)
        概要:インターフェイスのメソッドが呼ばれたかどうかチェックする場合に使用します。
        第1引数:呼び出されたどうか確認したいメソッド名を指定します。
        戻り値:booleanでメソッドが呼び出されたかどうかを返します。

   public Object[] getArgs(String methodName)
        概要:インターフェイスのメソッドの引数の値を取得したい場合に使用します。
        第1引数:呼び出されたソッドの引数の値を知りたい場合のメソッド名を指定します。
        戻り値:Object[]で呼び出されたメソッドの引数の値を返します。


モックの使用方法

インターフェイスのモックを作成するには、設定したMockInterceptorをアスペクトして使用します。例えば以下のようなインターフェイスがあったとします。

public interface Hello {
    public String greeting();
    public String echo(String str);
}

このインターフェイスのモックを作成する仕様として、greeting()メソッドが呼び出されたときは、"Hello"を返し、echo()メソッドが呼び出されたときは"Hoge"を返すこととする場合、以下のように作成します。

MockInterceptor mi = new MockInterceptor();
mi.setReturnValue("greeting", "Hello");
mi.setReturnValue("echo", "Hoge");
Hello hello = mi.createProxy(Hello.class);

上記のコードをコンポーネント定義で書くと次のようになります。

<component class="examples.aop.Hello">
    <aspect>
        <component class="org.seasar.framework.aop.interceptors.MockInterceptor">
            <initMethod name="setReturnValue">
                <arg>"greeting"</arg>
                <arg>"Hello"</arg>
            </initMethod>
            <initMethod name="setReturnValue">
                <arg>"echo"</arg>
                <arg>"Hoge"</arg>
            </initMethod>
        </component>
    </aspect>
</component>

詳しい使用方法は、Exampleのモックを参照してください。

S2Unitを使ったテストクラスの作成方法

テストクラスは、org.seasar.extension.unit.S2TestCaseを継承して作成します。またS2TestCaseはJUnitを拡張したクラスなので、テストメソッド等の使用方法はJUnitと同様です。

S2Unitの機能

S2Containerを使った開発のテストを簡単に行えるように以下の機能があります。

   ・S2Containerの自動生成
        S2Containerを生成する場合、テストメソッド(testXxx)ごとに自動的にS2Containerを作成します。
        S2ContainerFactory.create(PATH)と記述してS2Containerを生成する必要がありません。

   ・register(),getComponent(),include()メソッド
        S2Containerに対するregister(),getComponent(),include()メソッドが用意されています。

   ・include時のPATHの省略
        include()するPATHがテストクラスと同じパッケージにある場合は、パッケージ部分のパスは省略できます。
        例えばaaa.bbb.CccTestクラスがaaa/bbb/hoge.diconをinclude()する場合、include("hoge.dicon")と記述することができます。

   ・変数の自動セット
        TestCaseのstaticでなくfinalでもないフィールドが存在し、その名前からアンダースコア(_)を除いた名前のコンポーネントがコンテナ
        に存在すれば自動的にセットされます。
        TestCaseのフィールドにインターフェイスの変数が定義されていればS2Containerから取り出して自動的にセットされます。
        テストメソッドが終わると自動セットされた値は自動的にクリア(nullをセット)されます。

   ・setUpXxx(),tearDownXxx()メソッド
        テストメソッド(testXxx)に対応するsetUpXxx(),tearDownXxx()を定義しておくと、setUp()の後、tearDown()の前に自動的に呼び出さ
        れます。
        個別のテストメソッドごとの初期化・終了処理を簡単に行えるようになります。

データベースに対するテストを行うために以下の機能が用意されています。

   ・トランザクション制御
        include("j2ee.dicon")をしておき、テストメソッド名の最後にTxをつける(testXxxTx)ことにより、テストメソッドの直前にトランザク
        ションを開始し、テストメソッドの直後にトランザクションをロールバック行うので、データベースに関するテストを行った場合のクリー
        ンアップの処理が不要になります。

   ・assertEquals()メソッド
        assertEquals()で予想されるDataSetの結果に対して、Map、MapのList、Bean、BeanのListと比較することができます。

   ・readXls()メソッド
        DataSet expected = readXls("検証用.xls")のようにしてDataSetに検証用のExcelデータを読み込むことができます。
        検証用のExcelデータのPATHがテストクラスと同じパッケージにある場合PATHを省略できます。
        検証用のExcelデータを作成する場合はExcelファイルの作成を参照してください。

   ・reload()メソッド
        reload(DataSet)を使い、データの中身をプライマリーキーでリロードして新しいDataSetを取得できます。更新後の予想される結果
        をExcelで定義しておき、以下のようにして簡単に更新のテストができます。
                DataSet expected = readXls("検証用.xls");
                assertEquals(expected, reload(expected);

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

Excelファイルの作成

検証用のExcelファイルを作成するには、以下のように設定します。作成したExcelファイルはテストクラスと同じ場所に配置するのが一般的です。

@シート名
テーブル名を記述します。
Aシートの1行目
カラム名を記述します。

N:1のマッピングもベースとなるシートに「カラム名_関連番号」の名前で記述します。
Bシートの2行目以降
データを記述します。

データベースから検証用のExcelファイルを作成することが出来るように以下のクラスが用意されています。

  • データベースの内容をDataSetに読み込むクラス
  • DataSetをExcelファイルに出力するクラス
   データベースの内容をDataSetに読み込むクラス
      クラス名
            org.seasar.extension.dataset.impl.SqlReader

      メソッド詳細
            public void addTable(String tableName)
                説明:指定されたテーブル名のすべてのデータをDataSetに読み込みます。
                第1引数:取り出すデータのテーブル名を指定します。

            public void addTable(String tableName, String condition)
                説明:指定された条件で指定されたテーブルのデータをDataSetに読み込みます。
                第1引数:取り出すデータのテーブル名を指定します。
                第2引数:取り出す条件を指定します。

            public void addSql(String sql, String tableName)
                説明:指定されたテーブルを指定されたSelect文の条件でデータをDataSetに読み込みます。
                第1引数:テーブルからデータを取り出すためのSelect文を指定します。
                第2引数:取り出すデータのテーブル名を指定します。

            public DataSet read()
                説明:読み込んだDateSetを返します。
                戻り値:addTable()メソッドまたはaddSql()メソッドで読み込んだDateSetを返します。

   DataSetをExcelファイルに出力するクラス
      クラス名
            org.seasar.extension.dataset.impl.XlsWriter
      メソッド詳細
            (コンストラクタ)public XlsWriter(String path)
                説明:コンストラクタでファイルのパスを指定します。
                第1引数:出力するファイルパスを指定します。

            public void write(DataSet dataSet)
                説明:指定されたDataSetをコンストラクタで指定したファイルパスのExcelファイルに出力します。
                第1引数:出力するDataSetを指定します。

詳しい使用方法はExampleのデータベースに対するテストを参照してください。

Example

以下のサンプルを試す場合は、セットアップを参照して環境を作ってから行ってください。

モック

S2で用意されているMockInterceptorを使ってインターフェイスのモックを作成して、メソッドと引数が正しく呼ばれていかS2Unitを使ってテストを作成しましょう。作成するファイルは以下のとおりです。

  • インターフェイス(Hello.java)
  • インターフェイスにモックの定義を行うdiconファイル(Hello.dicon)
  •   
  • 作成したインターフェイスのテストを行うクラス(HelloTest.java)
インターフェイスの作成
  • 引数が無く、戻り値がStringのメソッド(greeting)を作成します。
  • 引数が1つで、戻り値がStringのメソッド(echo)を作成します。
Hello.java
package examples.aop.mockinterceptor;

public interface Hello {

    public String greeting();

    public String echo(String str);

}
diconファイルの作成
  • モックの仕様はgreeting()メソッドが呼び出されたときは、"Hello"を返し、echo()メソッドが呼び出されたときは"Hoge"を返すことにします。
  • MockInterceptorをコンポーネントに定義します。name属性はhelloMockInterceptorとします。
  • MockInterceptorのsetReturnValue()メソッドに対してメソッド・インジェクションを使ってモックの仕様どおりに引数を指定します。
  • インターフェイスをコンポーネントに定義します。aspectタグにコンポーネントに定義したMockInterceptorを指定します。
Hello.dicon
<?xml version="1.0" encoding="Shift_JIS"?>
<!DOCTYPE components PUBLIC "-//SEASAR//DTD S2Container//EN"
"http://www.seasar.org/dtd/components.dtd">
<components>
    <component name="mockInterceptor" 
               class="org.seasar.framework.aop.interceptors.MockInterceptor">
        <initMethod name="setReturnValue">
            <arg>"greeting"</arg>
            <arg>"Hello"</arg>
        </initMethod>
        <initMethod name="setReturnValue">
            <arg>"echo"</arg>
            <arg>"Hoge"</arg>
        </initMethod>
    </component>
    <component class="examples.aop.mockinterceptor.Hello">
        <aspect>
            mockInterceptor
        </aspect>
    </component>
</components>

テストクラスの作成
  • S2TestCaseを継承します。
  • diconファイルに設定したexamples.aop.Helloとorg.seasar.framework.aop.interceptors.MockInterceptorを自動的にセットするように変数を宣言します。
  • MockInterceptor#isInvoked(String methodName)を使ってメソッドが呼び出されたかどうか確認します。
  • MockInterceptor#getArgs(String methodName)を使ってメソッドの引数の値が正しいか確認します。
HelloTest.java
package examples.aop.mockinterceptor;

import org.seasar.extension.unit.S2TestCase;
import org.seasar.framework.aop.interceptors.MockInterceptor;

public class HelloTest extends S2TestCase{
    //テストクラスと同じパッケージにあるのでパスが省略できる
    private static String PATH = "Hello.dicon";
	
    //変数の自動セット
    private Hello hello ;
	
    private MockInterceptor mi ;
	
    public void testHello() throws Exception{
		
	//diconファイルでインターフェイスのモックが正しく行われているか
	assertEquals("Hello", hello.greeting());
	assertEquals("Hoge", hello.echo("test"));
		
	hello.echo("Hello");
	//echo()メソッドが呼ばれたかどうか
	assertEquals(true, mi.isInvoked("echo"));
		
	//echo()メソッドの引数の値が"Hello"かどうか
	assertEquals("Hello", mi.getArgs("echo")[0]);
		
    }
	
    protected void setUp() throws Exception {
          //S2Containerに対するinclude()メソッド
	include(PATH);
    }

    protected void tearDown() throws Exception {
    }
	
    public HelloTest(String arg0) {
	super(arg0);
    }

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

}

コンポーネントの組み立てやコンポーネントの取り出しといった作業はS2TestCaseが自動的に行うので、テストメソッドは必要なことにだけに集中できます。

実行結果

"OK (テスト数 test)"と出ていればassertEquals()メソッドで予想した通り結果が得られています。

.
Time: 1.643

OK (1 test)

この演習は、seasar2/src/examples/aop/mockinterceptor以下に用意されています。

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

以下のサンプルを動かすためにはHSQLDBを使用しますので、セットアップを参照してHSQLDBを起動させておいてください。ここでは、以下のEMP(従業員)テーブルとDEPT(部署)テーブルを使います。

テーブル:EMP(従業員)
カラム名 論理名 NotNull 主キー
EMPNO 従業員番号 NUMBER
ENAME 従業員名 VARCHAR2

JOB 仕事 VARCHAR2

MGR 上司 NUMBER

HIREDATE 雇用日 DATE

SAL 給料 NUMBER

COMM 手数料 NUMBER

DEPTNO 部署番号 NUMBER


テーブル:DEPT(部署)
カラム名 論理名 NotNull 主キー
DEPTNO 部署番号 NUMBER
DNAME 部署名 VARCHAR2

LOC ロケーション VARCHAR2

VERSIONNO バージョン番号 NUMBER


SQL文を発行するためのフレームワークとして最も単純なS2JDBCを使ってデータベースに対するテストを行います。テストをする内容は従業員テーブルを従業員番号で検索するDAOの結果について行います。検索を行うDAOのソースはseasar2/src/exapmles/unit/以下に配置してあります。シナリオとして従業員番号9900で検索をかけると、従業員番号9900の従業員テーブルと部署番号99の部署テーブルをジョインして返す想定とします。

テストを行う為の手順は以下のとおりになります。
        1.テスト用の従業員テーブルと部署テーブルのExcelデータの作成
        2.結果を検証するためのExcelデータの作成
        3.テストを行うクラスの作成、実行

1.テスト用の従業員テーブルと部署テーブルのExcelデータの作成

Excelのデータを用意するには、シート名がテーブル名で、シートの第1行にカラム名を2行目以降にデータを記述して作成できますが、ここでは既存のテーブルのデータを利用してテストデータを作成します。

テスト用のテーブルデータを作成するためのファイルは以下のとおりです。

  • データベースの内容をExcelに書き出す為のdiconファイル(Db2Excel.dicon)
  • diconファイルに設定した内容をExcelにデータを書き出す実行ファイル(Db2Excel.java)

diconファイルの作成

  • j2ee.diconをインクルードします。
  • SqlReaderクラスのコンポーネント定義をします。1レコードしか必要ないので条件として従業員テーブルは従業員番号を7788で部署テーブルは部署番号を20で検索した結果のデータとします。
  • XlsWriterクラスのコンポーネント定義をします。コンストラクタ・インジェクションで出力するファイルパスを指定します。
Db2Excel.dicon
<?xml version="1.0" encoding="Shift_JIS"?>
<!DOCTYPE components PUBLIC "-//SEASAR//DTD S2Container//EN"
"http://www.seasar.org/dtd/components.dtd">
<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>

実行ファイルの作成

  • org.seasar.framework.container.S2Container#create()メソッドの最初の引数に作成したdiconファイル(Db2Excel.dicon)のパスを指定してコンテナを作成します。
  • org.seasar.framework.container.S2Container#getComponent()メソッドの第1引数にコンポーネントに登録したクラス名(SqlReader.class)を指定して取得します。
  • org.seasar.framework.container.S2Container#getComponent()メソッドの第1引数にコンポーネントに登録したクラス名(XlsWriter.class)を指定して取得します。
  • コンテナから取得したXlsWriter#write()メソッドの第1引数にSqlReader#read()を指定して実行します。
Db2Excel.java
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();
        }
    }
}

実行結果

../src/test/examples/unit/getEmployeePrepare.xlsが以下のように作成されていることを確認してください。

従業員番号9900で検索をかけると、従業員番号9900の従業員テーブルと部署番号99の部署テーブルをジョインして返す想定としているので、empシートのEMPNOを9900とDEPTNOを99、deptシートのDEPTNOを99に書き換えて保存します。

2.結果を検証するためのExcelデータの作成

結果を検証するためのデータを用意します。このデータも手書きで用意できますが先ほどと同じように作成します。結果を検証するデータを作成するためのファイルは以下のとおりです。

  • 先ほど作成したDb2Excel.diconを変更して、結果を検証するためのデータをExcelに書き出すようにするdiconファイル(Db2Excel.dicon)

diconファイルの作成

  • SqlReaderクラスのaddSql()メソッドを使用してSqlReaderクラスのコンポーネントの定義をしなおします。
  • XlsWriterクラスのコンポーネント定義で出力するファイルパスを変更します。
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/getEmployeePrepare.xls"</arg>
    </component>
</components>

先ほど作成した実行ファイル(Db2Excel.java)を実行します。

実行結果

../src/test/examples/unit/getEmployeePrepare.xlsが以下のように作成されていることを確認してください。

従業員番号9900で検索をかけると、従業員番号9900の従業員テーブルと部署番号99の部署テーブルをジョインして返す想定としているので、empシートのEMPNOを9900、DEPTNOを99に書き換えて保存します。

3.テストを行うクラスの作成

テスト用の従業員テーブルと部署テーブルのExcelデータと結果を検証するExcelデータが作成できたので、それを使って実際にDaoを呼び出して取得したデータの検証をするテストクラスを作成します。

テストクラスの作成

  • S2TestCaseを継承します。
  • setUp()でdiconファイルをインクルードします。
  • S2Unitのトランザクション制御機能を使ってテスト用の従業員テーブルと部署テーブルのExcelデータをデータベースに格納してもテストメソッドが終了した直後にトランザクションがロールバックされるようにテストメソッド名の最後にTxを付ける。
  • readXlsWriteDb()メソッドを使ってテスト用のExcelデータをデータベースに登録します。
  • readXls()メソッドを使って検証用のExcelデータをDataSetに読み込ませます。
  • assertEquals()メソッドを使って読み込ませたDataSetとDaoを呼び出して取得したデータを比較します。
EmployeeDaoImplTest.java
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);
    }
}

実行結果

"OK (テスト数 test)"と出ていればassertEquals()メソッドで予想した通りの結果が得られています。

.DEBUG 2004-10-08 13:10:00,762 [main] トランザクションを開始しました
DEBUG 2004-10-08 13:10:05,379 [main] 物理的なコネクションを取得しました
DEBUG 2004-10-08 13:10:05,469 [main] 論理的なコネクションを取得しました
DEBUG 2004-10-08 13:10:05,990 [main] 論理的なコネクションを閉じました
DEBUG 2004-10-08 13:10:06,140 [main] INSERT INTO emp(EMPNO, ENAME, JOB, MGR, HIREDATE, SAL, 
COMM, DEPTNO) VALUES(9900, 'SCOTT2', 'ANALYST', 7566, '1982-12-09 00.00.00', 3000, null, 99)
DEBUG 2004-10-08 13:10:06,140 [main] 論理的なコネクションを取得しました
DEBUG 2004-10-08 13:10:06,901 [main] 論理的なコネクションを閉じました
DEBUG 2004-10-08 13:10:06,911 [main] 論理的なコネクションを取得しました
DEBUG 2004-10-08 13:10:07,151 [main] 論理的なコネクションを閉じました
DEBUG 2004-10-08 13:10:07,151 [main] INSERT INTO dept(DEPTNO, DNAME, LOC) VALUES(99, 'RESEARCH2',
'DALLAS')
DEBUG 2004-10-08 13:10:07,151 [main] 論理的なコネクションを取得しました
DEBUG 2004-10-08 13:10:07,151 [main] 論理的なコネクションを閉じました
DEBUG 2004-10-08 13:10:07,151 [main] SELECT e.empno, e.ename, e.deptno, d.dname FROM emp e, dept d
WHERE e.empno = 9900 AND e.deptno = d.deptno
DEBUG 2004-10-08 13:10:07,151 [main] 論理的なコネクションを取得しました
DEBUG 2004-10-08 13:10:07,312 [main] 論理的なコネクションを閉じました
DEBUG 2004-10-08 13:10:07,392 [main] トランザクションをロールバックしました
DEBUG 2004-10-08 13:10:07,492 [main] 物理的なコネクションを閉じました

Time: 13.87

OK (1 test)

このようにreadXls()で結果検証用のExcelデータを読み込み、Daoの結果と比較することが出来ることが確認できます。また、テストのためにデータベースに格納したデータもすべてロールバックしてもとに戻るため、データのクリーンアップを考える必要がなくなります。

この演習は、seasar2/src/test/examples/unit以下に用意されています。