Seasar DI Container with AOP

S2Unit.NETでテストを楽しく簡単に

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

  • S2Containerの自動作成
    • テストメソッド(TestXxx)ごとに自動的にS2Containerを作成します。
    • S2Containerに対するRegister(),GetComponent(),Include()が用意されています。
    • Include()するdiconファイルのPATHがテストクラスと同じ名前空間にある場合は、名前空間部分のパスは省略できます。
  • フィールドへの自動バインディング
    • Testクラスにstatic,readonly,finalのいずれでもないフィールドがあり、 その名前からアンダースコア(_)を(先頭もしくは後尾から)除いた名前のコンポーネントがコンテナに存在すれば自動的にセットされます。
    • Testクラスのフィールドに、代入することのできるコンポーネント(参照型かDateTime)がコンテナに存在すれば、S2Containerから取り出して自動的にセットされます。
    • テストメソッドが終わると、自動セットされた値は自動的にnull(VBの場合はNothing)がセットされます。
  • テストメソッド毎の初期化・終了処理
    • テストメソッド(TestXxx)に対応するSetUpXxx(),TearDownXxx()を定義しておくと、 SetUp()の後、TearDown()の前に自動的に呼び出されます。 個別のテストメソッドごとの初期化・終了処理を簡単に行えるようになります。
  • トランザクションの自動制御
    • テストメソッドのS2属性にTx列挙型(Seasar.Extension.Unit.Tx)のRollback(列挙子)を指定すると、 テストメソッドの直前にトランザクションを開始し、テストメソッドの直後にトランザクションをロールバックするので、 データベースに関するテストを行った場合のクリーンアップの処理が不要になります。
  • データベースに対するテスト
    • Reload(DataSet)を使って、データの中身をプライマリーキーでリロードして新しいDataSetを取得できます。 更新後の予想される結果をExcelで定義しておき、 DataSet expected = ReadXls("予想される結果.xls"); S2Assert.AreEqual(expected, Reload(expected); のようにして簡単に更新のテストができます。
    • S2Assert.AreEqual()で予想されるDataSetの結果に対して、 IDictionary、IDictionaryのIList、object、objectのIListと比較することができます。

事前の準備

S2Unit.NETではMbUnit 2.3.0.0を使用しています。 事前にMbUnitをインストールしておく必要があります。

テストクラスはSeasar.Extension.Unit.S2TestCaseクラスを継承します。 S2TestCaseクラスはSeasar.Unit.dllに含まれてるので、テストコードを含むプロジェクトではSeasar.Unit.dllを参照に追加して下さい。

S2Containerの自動作成

テストクラスはSeasar.Extension.Unit.S2TestCaseを継承して作成します。 クラスにはもちろんTestFixture属性(MbUnit.Framework.TestFixtureAttribute)も定義します。 テストメソッドにはTest属性(MbUnit.Framework.TestAttribute)と共に、 S2属性(Seasar.Extension.Unit.S2Attribute)を定義します。 このようなテストクラスを準備するとS2Containerが自動的に作成されます。

C#
[TestFixture]
public class HogeTest : S2TestCase
{
    [Test, S2]
    public void TestHogeHoge()
    {
        // テストコードを書きます
    }
}
VB.NET
<TestFixture()> _
Public Class HogeTest
   Inherits S2TestCase
   
    <Test(), S2()> _
    Public sub TestHogeHoge()
        ' テストコードを書きます
    End Sub
    
End Class

S2TestClassにはS2Container.NETに対応するRegister(), GetComponent(), Include() が用意されています。

C#
public void TestHogeHoge()
{
    // diconファイルを取り込みます
    Include("Hoge.dicon");
    
    // S2Containerにコンポーネントを登録します
    Register(typeof(Hashtable));
    
    // S2Containerからコンポーネントを取得します
    Hashtable table = (Hashtable) GetComponent(typeof(Hashtable));
}
VB.NET
<Test(), S2()> _
Public sub TestHogeHoge()

    ' diconファイルを取り込みます
    Include("Hoge.dicon")
    
    ' S2Containerにコンポーネントを登録します
    Register(typeof(Hashtable))
    
    ' S2Containerからコンポーネントを取得します
    Dim table As Hashtable = CType(GetComponent(GetType(Hashtable)), Hashtable)
    
    ' 明示的にキャストしない場合は以下でも可
    ' Dim table As Hashtable = GetComponent(GetType(Hashtable))

End Sub

Include()するdiconファイルのPATHがテストクラスと同じ名前空間にある場合は、名前空間部分のパスは省略できます。 ファイルシステムでdiconファイルのPATHを指定する場合は、アセンブリと同じ位置の置くと相対パスとなり省略することができます。 (ただしビルドイベント等でアセンブリと同じフォルダにはき出す等の処理が必要です)

Foo.Fuga名前空間にTest.diconを用意している場合、 Foo.Fuga名前空間にあるクラスでは以下のようにdiconファイルのパスを省略することが出来ます。

C#
namespace Foo.Fuga
{
    [TestFixture]
    public class HogeTest : S2TestCase
    {
        [Test, S2]
        public void TestHogeHoge()
        {
            Include("Test.dicon");
        }
    }
}
VB.NET
Namespace Foo.Fuga
   <TestFixture()> _
   Public Class HogeTest
      Inherits S2TestCase
      
      <Test(), S2()> _
      Public Sub TestHogeHoge()
         Include("Test.dicon")
      End Sub
      
   End Class
End Namespace

フィールドへの自動バインディング

Testクラスにstatic,readonly,finalのいずれでもないフィールドがあり、 その名前からアンダースコア(_)を(先頭もしくは後尾から)除いた名前のコンポーネントがコンテナに存在すれば自動的にセットされます。

Test.dicon
<components>
    <component name="abc">"hoge"</component>
</components>
C#
[TestFixture]
public class HogeTest : S2TestCase
{
    private string _abc = null;

    [Test, S2]
    public void TestHogeHoge()
    {
        Include("Test.dicon");
        Assert.AreEqual("hoge", _abc);
    }
}
VB.NET
<TestFixture()> _
Public Class HogeTest
   Inherits S2TestCase
   
    Private _abc As String = Nothing
   
    <Test(), S2()> _
    Public Sub TestHogeHoge()
        Include("Test.dicon")
        Assert.AreEqual("hoge", _abc)
    End Sub

End Class

Testクラスのフィールドに、代入することのできるコンポーネント(参照型かDateTime)がコンテナに存在すれば、S2Containerから取り出して自動的にセットされます。

Test.dicon
<components>
    <component class="System.Collections.ArrayList" />
</components>
C#
[TestFixture]
public class HogeTest : S2TestCase
{
    private IList _list = null;

    [Test, S2]
    public void TestHogeHoge()
    {
        Include("Test.dicon");
        Assert.IsNotNull(_list);
    }
}
VB.NET
<TestFixture()>  _
Public Class HogeTest
    Inherits S2TestCase

    Private _list As IList = Nothing

    <Test(), S2()> _
    Public Sub TestHogeHoge()
        Include("Test.dicon")
        Assert.IsNotNull(_list)
    End Sub

End Class

テストメソッドが終わると、自動セットされた値は自動的にnull(VBの場合はNothing)がセットされます。

テストメソッド毎の初期化・終了処理

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

C#
[TestFixture]
public class HogeTest : S2TestCase
{
    public void SetUpHogeHoge() 
    {
        // 初期化処理を行います 
    }

    [Test, S2]
    public void TestHogeHoge()
    {
        // テストコードを書きます
    }

    public void TearDownHogeHoge()
    {
        // 終了処理を書きます
    }
}
VB.NET
<TestFixture()> _
Public Class HogeTest
    Inherits S2TestCase
   
    Public Sub SetUpHogeHoge()
        ' 初期化処理を行います
    End Sub 

    <Test(), S2()> _
    Public Sub TestHogeHoge()
        ' テストコードを書きます
    End Sub

    Public Sub TearDownHogeHoge()
        ' 終了処理を書きます
    End Sub

End Class

トランザクションの自動制御

テストメソッドのS2属性にTx列挙型(Seasar.Extension.Unit.Tx)のRollback(列挙子)を指定すると、 テストメソッドの直前にトランザクションを開始し、テストメソッドの直後にトランザクションをロールバックするので、 データベースに関するテストを行った場合のクリーンアップの処理が不要になります。

Rollbackの他に、Tx列挙型のCommit(列挙子)を指定すると、自動的にトランザクションをコミットします。 NotSupported(列挙子)を指定すると、トランザクションは開始しません。 S2属性にTx列挙型の列挙子を与えない場合は、デフォルトでNotSupported(列挙子)を指定したのと同じ動作をします。

またトランザクションを扱うためには、下記のようにTransactionContextとDataSourceを定義したdiconファイルを 読み込んでおく必要があります。

Ado.dicon
<components namespace="Ado">

    <component name="SqlClient" class="Seasar.Extension.ADO.DataProvider">
        <property name="ConnectionType">"System.Data.SqlClient.SqlConnection"</property>
        <property name="CommandType">"System.Data.SqlClient.SqlCommand"</property>
        <property name="ParameterType">"System.Data.SqlClient.SqlParameter"</property>
        <property name="DataAdapterType">"System.Data.SqlClient.SqlDataAdapter"</property>
    </component>
    
    <component name="DataSource" class="Seasar.Extension.Tx.Impl.TxDataSource">
        <property name="DataProvider">SqlClient</property>
        <property name="ConnectionString">
            "Server=サーバ名;database=s2dotnetdemo;Integrated Security=SSPI"
       </property>
    </component>

    <component class="Seasar.Extension.Tx.Impl.TransactionContext" />
    
</components>
C#
[TestFixture]
public class HogeTest : S2TestCase
{
    public void SetUpHogeHoge() 
    {
        Include("Ado.dicon");
    }

    [Test, S2(Tx.Rollback)]
    public void TestHogeHoge()
    {
        // メソッド終了後に自動的にロールバックされます
    }
}
VB.NET
<TestFixture> _ 
Public Class HogeTest
     Inherits S2TestCase

    Public  Sub SetUpHogeHoge()
        Include("Ado.dicon")
    End Sub
 
    <Test, S2(Tx.Rollback)> _ 
    Public  Sub TestHogeHoge()
        ' メソッド終了後に自動的にロールバックされます
    End Sub

End Class

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

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

Select文に対するテスト

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

Seasar/Examples/Reference/S2Unit/Db2ExcelClient.dicon
<components>
  <include path="Seasar.Examples/Ado.dicon" />
  <component name="Db2ExcelClient" class="Seasar.Examples.Reference.S2Unit.Db2ExcelClient"/>
  <component class="Seasar.Extension.DataSets.Impl.SqlReader">
    <initMethod>self.AddTable("emp", "empno = 7788")</initMethod>
    <initMethod>self.AddTable("dept", "deptno = 20")</initMethod>
  </component>
  <component class="Seasar.Extension.DataSets.Impl.XlsWriter" instance="prototype">
    <arg>"Seasar.Examples/Reference/S2Unit/GetEmployeePrepare.xls"</arg>
  </component>
</components>

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

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

Seasar/Examples/Reference/S2Unit/Db2ExcelClient
using System;
using Seasar.Extension.DataSets.Impl;
using Seasar.Framework.Container;
using Seasar.Framework.Container.Factory;

namespace Seasar.Examples.Reference.S2Unit
{
    public class Db2ExcelClient
    {
        private static readonly string PATH = "Seasar.Examples/Reference/S2Unit/Db2ExcelClient.dicon";

        public Db2ExcelClient() { }

        public void Main()
        {
            IS2Container container = S2ContainerFactory.Create(PATH);
            container.Init();
            try
            {
                SqlReader reader = (SqlReader) container.GetComponent(typeof(SqlReader));
                XlsWriter writer = (XlsWriter) container.GetComponent(typeof(XlsWriter));
                writer.Write(reader.Read());
                Console.Out.WriteLine("output Excel File : {0}", writer.FullPath);
            }
            catch (ApplicationException e)
            {
                Console.Out.WriteLine(e.Message);
            }
        }
    }
}

IS2ContainerからSqlReaderを取り出しRead()、XlsWriterを取り出しWrite()するだけで、 データベースの内容をExcelに書き出すことができます。 Seasar.Examplesを起動し、ツリーアイテムからS2Containerリファレンス-Select文に対するテスト(1)、を選択すると Visual Studioの出力パス+指定パス(例:bin\Debug\Seasar.Examples\Reference\S2Unit)に GetEmployeePrepare.xlsが作成されていることが確認できると思います。 GetEmployeePrepare.xlsをダブルクリックするとExcelが起動します。 empシートのEMPNOを9900、ENAMEをSCOTT2、DEPTNOを99に変更します。 続いてdeptシートのDEPTNOを99、DNAMEをRESEARCH2に変更します。これで検索用の元データは用意できます。Excelで保存を選び、終了します。

次に結果を検証するためのデータを用意します。 Seasar.Examplesに検証データを書き出すDb2ExcelClient2が用意されているのでそれを使います。

Seasar/Examples/Reference/S2Unit/Db2ExcelClient2.dicon
<components>
  <include path="Seasar.Examples/Ado.dicon" />
  <component name="EmployeeDaoTest" class="Seasar.Examples.Reference.S2Unit.EmployeeDaoTest"/>
  <component class="Seasar.Examples.Reference.S2Unit.EmployeeDao">
    <property name="GetEmployeeHandler">
      <component class="Seasar.Extension.ADO.Impl.BasicSelectHandler">
        <property name="Sql">
"SELECT e.empno, e.ename, e.deptno, d.dname
FROM emp e, dept d WHERE e.empno = @empno AND e.deptno = d.deptno"</property>
      </component>
    </property>
  </component>
</components>

Seasar.Examplesを起動し、ツリーアイテムからS2Containerリファレンス-Select文に対するテスト(2)、を選択すると Visual Studioの出力パス+指定パス(例:bin\Debug\Seasar.Examples\Reference\S2Unit)にGetEmployeeResult.xlsを作成します。

先ほどと同様な手順でGetEmployeeResult.xlsのempシートのEMPNOを9900、ENAMEをSCOTT2、DEPTNOを99、DNAMEをRESEARCH2に書き換えて保存します。 これで、テスト用のデータがそろいました。いよいよテストに取り掛かります。

Seasar/Examples/Reference/S2Unit/EmployeeDao.dicon
<components>
  <include path="Seasar.Examples/Ado.dicon" />
  <component name="EmployeeDaoTest" class="Seasar.Examples.Reference.S2Unit.EmployeeDaoTest"/>
  <component class="Seasar.Examples.Reference.S2Unit.EmployeeDao">
    <property name="GetEmployeeHandler">
      <component class="Seasar.Extension.ADO.Impl.BasicSelectHandler">
        <property name="Sql">
"SELECT e.empno, e.ename, e.deptno, d.dname 
FROM emp e, dept d WHERE e.empno = @empno AND e.deptno = d.deptno"</property>
      </component>
    </property>
  </component>
</components>
Seasar/Examples/Reference/S2Unit/EmployeeDaoTest.cs
using System.Data;
using MbUnit.Core.Cons;
using MbUnit.Framework;
using Seasar.Extension.DataSets.Impl;
using Seasar.Extension.Unit;

namespace Seasar.Examples.Reference.S2Unit
{
    [TestFixture]
    public class EmployeeDaoTest : S2TestCase
    {
        private IEmployeeDao dao_ = null;

        public void SetUpGetEmployee()
        {
            Include("Seasar.Examples/Reference/S2Unit/EmployeeDao.dicon");
        }

        [Test, S2(Tx.Rollback)]
        public void GetEmployee()
        {
            ReadXlsWriteDb("Seasar.Examples/Reference/S2Unit/GetEmployeePrepare.xls");
            Employee emp = dao_.GetEmployee(9900);
            DataSet expected = ReadXls("Seasar.Examples/Reference/S2Unit/GetEmployeeExpected.xls");
            S2Assert.AreEqual(expected, emp, "1");
        }

        public void Main()
        {
            using (MainClass mc = new MainClass())
            {
                mc.Main(new string[] { "Seasar.Examples.exe" });
            }
        }
    }
}

Seasar.Examplesを起動し、ツリーアイテムからS2Containerリファレンス-Select文に対するテスト(3)、を選択するとテストが実行されます。 ここでは説明のためにコンソールからMbUnitを実行していますが、 MbUnit.GUI.exeやTestDriven.NETを使用したほうがテストしやすいでしょう。

SetUpGetEmployee()がapp.diconの役割を担います。S2Assert.AreEqual()でDataSetとIDictionary、IDictionaryのIList、object、objectのIList と比較できるのですっきりとしたテストコードになります。

ReadXlsWriteDb()、ReadXlsAllReplaceDb()で、テストのために用意したデータをデータベースに格納します。 Excelのファイルがテストクラスと同じ名前空間にある場合は、名前空間部分のパスは省略できます。 (ただしビルドイベント等でアセンブリと同じフォルダにはき出す等の処理が必要です) ReadXlsWriteDb()、ReadXlsAllReplaceDb()はテスト後にロールバックしてデータが元に戻るようにテストメソッドの最初に実行してください。 これらのメソッドは、シート名の昇順にデータを挿入します。外部キー制約に引っかかるためデータの挿入順序を制御する場合、 シート名の先頭に"# {順序}"を付けます。例えば、empテーブルを3番目に挿入する場合、シート名は"#3 emp"になります。

Daoを呼び出して取得したデータとReadXls()で読み込んだ結果検証用のExcelデータをS2Assert.AreEqual()に渡すと Daoを呼び出して取得したデータがDataSetに変換され、結果検証用のExcelデータと比較します。