目次


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

  • S2Unit.NETのインストール
  • 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と比較することができます。
  • 制限事項

 テストクラスは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

<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

<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

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

<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

<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#

// SetUpFirst(), SetUp(), SetUpHogeHoge(), TestHogeHoge(),
// TearDownHogeHoge(), TearDown(), TearDownLast()の順に呼び出します。
[TestFixture]
public class HogeTest : S2TestCase
{
    [MbUnit.Framework.SetUp]
    public void SetUpFirst()
    {
        // 初期化処理を行います (MbUnitが呼び出します)
    }

    public void SetUp()
    {
        // 初期化処理を行います (S2Unit.NETが呼び出します。)
        // S2Container.NETに対する初期化処理はこのメソッドで呼び出します。
    }
    
    public void SetUpHogeHoge() 
    {
        // テストメソッド毎の初期化処理を行います 
    }

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

    public void TearDownHogeHoge()
    {
        // テストメソッド毎の終了処理を書きます
    }

    public void TearDown()
    {
        // 終了処理を書きます(S2Unit.NETが呼び出します。)
        // S2Container.NETに対する終了処理はこのメソッドで呼び出します。
    }

    [MbUnit.Framework.TearDown]
    public void TearDownLast()
    {
        // 終了処理を書きます (MbUnitが呼び出します)
    }
}

VB

' SetUpFirst(), SetUp(), SetUpHogeHoge(), TestHogeHoge(),
' TearDownHogeHoge(), TearDown(), TearDownLast()の順に呼び出します。
<TestFixture> _ 
Public Class HogeTest
     Inherits S2TestCase
    <MbUnit.Framework.SetUp> _ 

    Public  Sub SetUpFirst()
        ' 初期化処理を行います (MbUnitが呼び出します)
    End Sub
 
    Public  Sub SetUp()
        ' 初期化処理を行います (S2Unit.NETが呼び出します。)
        ' S2Container.NETに対する初期化処理はこのメソッドで呼び出します。
    End Sub
 
    Public  Sub SetUpHogeHoge()
        ' テストメソッド毎の初期化処理を行います 
    End Sub
 
    <Test, S2> _ 
    Public  Sub TestHogeHoge()
        ' テストコードを書きます
    End Sub
 
    Public  Sub TearDownHogeHoge()
        ' テストメソッド毎の終了処理を書きます
    End Sub
 
    Public  Sub TearDown()
        ' 終了処理を書きます(S2Unit.NETが呼び出します。)
        ' S2Container.NETに対する終了処理はこのメソッドで呼び出します。
    End Sub
 
    <MbUnit.Framework.TearDown> _ 
    Public  Sub TearDownLast()
        ' 終了処理を書きます (MbUnitが呼び出します)
    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

<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から手でデータを作成してもいいのですが、ここでは既存のテーブルのデータを利用してテストデータを作成します。 セットアップ-Examples を参照してデータベースを作成しておきます。 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 const 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()はテスト後にロールバックしてデータが元に戻るようにテストメソッドの最初に実行してください。

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

 S2Unit.NETは、S2Unit.Javaと異なりExcelのシートの定義順にデータを読み書きしません。 シート名の昇順にデータを読み書きします。

 データベースの外部キー制約に引っかかる等の理由でデータの挿入順序を制御する場合、 シート名の先頭に"# {順序}"を付けます。例えば、empテーブルのデータを3番目に挿入する場合、シート名は"#3 emp"になります。

Excelシートに定義したデータの読み書き順

 S2Unit.NETは、S2Unit.Javaと異なりExcelのシートの定義順にデータを読み書きしません。 シート名の昇順にデータを読み書きします。

 データベースの外部キー制約に引っかかる等の理由でデータの挿入順序を制御する場合、 シート名の先頭に"# {順序}"を付けます。例えば、empテーブルのデータを3番目に挿入する場合、シート名は"#3 emp"になります。

Excelシートに定義したデータの有効桁数

 S2Assert.AreEqualを使用してExcelシートに定義したデータとデータベースのデータを比較する場合、次の制限事項があります。

  • データ値が整数の場合、有効桁数は15桁までです。
  • データ値が小数の場合、有効桁数は7桁までです。
  • データ値が文字列の場合、有効文字数は32767文字までです。
  • データ値が日付/時刻の場合、有効日時は1900年1月1日から9999年12月31日までです(※比較するデータベースに依存します)。 また、有効精度は、秒までです(ミリ秒は切り捨てて比較します)。