ObjectMotherとSqlBulkCopyによるテストデータの一元管理(^o^)

ObjectMotherとSqlBulkCopyクラスをうまく利用すれば、テストデータの一元管理が可能になる。

DBアクセスを伴うテストに使用するテストデータもテストデータファイルを読み込むのではなく、SetUpでObjectMotherでテストデータを設定したDataTableをSqlBulkCopyクラスに食わせてDBに挿入し、TearDownでDBからテストデータを削除するようなDataAccessTestBaseクラスを用意する。

まずはObjectMotherのコード

using System;
using System.Data;

namespace TableModuleSample.Tests
{
    public class ObjectMother
    {
        private static readonly DataSet ds_;

        static ObjectMother()
        {
            ds_ = new DataSet();
            SetSchema();
        }

        private static void SetSchema()
        {
            DataTable products = new DataTable("Products");
            products.Columns.Add("ProductId", typeof(int));
            products.Columns.Add("ProductName", typeof(string));
            ds_.Tables.Add(products);
            DataTable orders = new DataTable("Orders");
            orders.Columns.Add("OrderId", typeof(int));
            orders.Columns.Add("CustomerName", typeof(string));
            orders.Columns.Add("OrderDate", typeof(DateTime));
            orders.Columns.Add("ShippedDate", typeof(DateTime));
            ds_.Tables.Add(orders);
            DataTable orderDetails = new DataTable("OrderDetails");
            orderDetails.Columns.Add("OrderId", typeof(int));
            orderDetails.Columns.Add("ProductId", typeof(int));
            orderDetails.Columns.Add("UnitPrice", typeof(decimal));
            orderDetails.Columns.Add("Quantity", typeof(decimal));
            orderDetails.Columns.Add("Discount", typeof(decimal));
            ds_.Tables.Add(orderDetails);
        }

        ...

        public static DataSet CreateOrderDetails()
        {
            DataSet ds = new DataSet();
            DataTable orderDetails = ds_.Tables["OrderDetails"].Clone();
            ds.Tables.Add(orderDetails);
            ds.Tables["OrderDetails"].Rows.Add(1, 1, 10000000m, 10m, 0.2m);
            ds.Tables["OrderDetails"].Rows.Add(1, 2, 11000000m, 20m, 0.3m);
            ds.Tables["OrderDetails"].Rows.Add(1, 3, 11500000m, 25m, 0.1m);
            ds.Tables["OrderDetails"].Rows.Add(2, 1, 10000000m, 10m, 0.2m);
            ds.Tables["OrderDetails"].Rows.Add(2, 2, 11000000m, 20m, 0.3m);
            ds.Tables["OrderDetails"].Rows.Add(2, 3, 11500000m, 25m, 0.1m);
            ds.AcceptChanges();

            return ds;
        }

        ...
    }
}

次に、DBアクセスを伴うテストのスーパークラスとなるDataAccessTestBaseクラスのコード

using System;
using System.Configuration;
using System.Data;
using System.Data.SqlClient;
using NUnit.Framework;

namespace TableModuleSample.Tests.Gateway
{
    public abstract class DataAccessTestBase<TargetType>
    {
        protected TargetType target_;

        [SetUp]
        public void データアクセステストの前処理()
        {
            target_ = CreateTarget();
            InsertTestData();
        }

        public abstract TargetType CreateTarget();

        private void InsertTestData()
        {
            using (SqlConnection connection = new SqlConnection(
                ConfigurationManager.ConnectionStrings["ConnectionString"].ConnectionString))
            {
                connection.Open();

                using (SqlTransaction transaction = connection.BeginTransaction())
                {
                    SqlBulkCopy copier = new SqlBulkCopy(connection, SqlBulkCopyOptions.Default, transaction);

                    foreach (DataTable table in Data.Tables)
                    {
                        copier.DestinationTableName = table.TableName;
                        copier.WriteToServer(table);
                    }

                    transaction.Commit();
                }
            }
        }

        public abstract DataSet Data { get; }

        [TearDown]
        public void データアクセステストの後処理()
        {
            ClearTestData();
        }

        private void ClearTestData()
        {
            using (SqlConnection connection = new SqlConnection(
                ConfigurationManager.ConnectionStrings["ConnectionString"].ConnectionString))
            {
                connection.Open();

                using (SqlTransaction transaction = connection.BeginTransaction())
                {
                    foreach (DataTable table in Data.Tables)
                    {
                        SqlCommand command = connection.CreateCommand();
                        command.Transaction = transaction;
                        command.CommandType = CommandType.Text;
                        command.CommandText = string.Format("TRUNCATE TABLE {0}", table.TableName);
                        command.ExecuteNonQuery();
                    }

                    transaction.Commit();
                }
            }
        }
    }
}

最後にGatewayのテストクラス。スーパークラスであるDataAccessTestBaseクラスの抽象メソッドCreateTargetをオーバーライドしてターゲットクラスを指定し、DataプロパティでDBに流し込む対象のDataSetをObjectMotherから取得している。

using System;
using System.Data;
using NUnit.Framework;
using NUnit.Framework.SyntaxHelpers;
using TableModuleSample.Core.Gateway;

namespace TableModuleSample.Tests.Gateway
{
    [TestFixture]
    public class OrderDetailGatewayTest : DataAccessTestBase<OrderDetailGateway>
    {
        public override OrderDetailGateway CreateTarget()
        {
            return new OrderDetailGateway();
        }

        public override DataSet Data
        {
            get { return ObjectMother.CreateOrderDetails(); }
        }

        [Test, Category("DBアクセス")]
        public void すべての注文詳細データをロードできるべき()
        {
            target_.LoadAll();

            Assert.That(target_.Table.Rows, Has.Count(6));
            Assert.That(target_.Table.Rows, Is.Unique);
        }

        [Test, Category("DBアクセス")]
        public void プライマリキーを指定して注文詳細データを抽出できるべき()
        {
            target_.LoadByPrimaryKey(1, 2);

            Assert.That(target_.Table.Rows, Has.Count(1));
            Assert.That(target_.Table.Rows[0]["OrderId"], Is.EqualTo(1));
            Assert.That(target_.Table.Rows[0]["ProductId"], Is.EqualTo(2));
            Assert.That(target_.Table.Rows[0]["UnitPrice"], Is.EqualTo(11000000m));
            Assert.That(target_.Table.Rows[0]["Quantity"], Is.EqualTo(20m));
            Assert.That(target_.Table.Rows[0]["Discount"], Is.EqualTo(0.3m));
        }

        [Test, Category("DBアクセス")]
        public void 注文IDを指定してデータを抽出できるべき()
        {
            target_.LoadByOrderId(1);

            Assert.That(target_.Table.Rows, Has.Count(3));

            foreach (DataRow orderDetail in target_.Table.Rows)
            {
                Assert.That(orderDetail["OrderId"], Is.EqualTo(1));
            }
        }

        [Test, Category("DBアクセス")]
        public void 既存のDataSetHolderを利用できるべき()
        {
            DataSetHolder holder = new DataSetHolder();
            target_ = new OrderDetailGateway(holder);
            target_.LoadByPrimaryKey(1, 2);

            DataRow orderDetail = target_.Table.Rows[0];
            orderDetail.Delete();

            holder.Update();

            OrderDetailGateway gateway = new OrderDetailGateway();
            gateway.LoadByPrimaryKey(1, 2);

            Assert.That(gateway.Table.Rows, Has.Count(0));
        }
    }
}