C#と諸々

C#がメインで他もまぁ諸々なブログです
おかしなこと書いてたら指摘してくれると嬉しいです(´・∀・`)
つーかコメント欲しい(´・ω・`)

2009/01/05 18:15

実践!ソフトウェアアーキテクチャ VisualStudioとASP.NETによる業務システム開発方法

この本でユニットテスト時に SMO を使ってテスト用のデータベースを構築しているのを見て、僕も取り入れてみたので自分のためにメモ。
# 本に載ってる方法とは少し違う方法。

ユニットテストだけでなく、インストーラのカスタム動作でも使えるので便利。


まず、データベース管理用のクラスライブラリプロジェクトを作成。ここでは Hoge.Databases と名付けることにする。

次に、全てのテーブルを (再) 作成するためのクエリーファイルをプロジェクト内に作成。ビルドアクションは "埋め込まれたリソース" に設定。ここでは Create.sql と名付けることにする。

SET ANSI_NULLS ON
GO


-- テーブルを削除。子テーブルを先に削除すること。
IF  EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[Table2]') AND type in (N'U'))
    DROP TABLE [dbo].[Table2]
GO

IF  EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[Table1]') AND type in (N'U'))
    DROP TABLE [dbo].[Table1]
GO


-- テーブルを作成。親テーブルを先に作成すること。
CREATE TABLE [dbo].[Table1]
(
    [PrimaryKey] [bigint] IDENTITY(1,1) NOT NULL,
    [Id] [nvarchar](255) NOT NULL,
    [Name] [nvarchar](255) NOT NULL,
    
    CONSTRAINT [PK_Table1] PRIMARY KEY CLUSTERED ([PrimaryKey]),
    CONSTRAINT [IX_Table1_Id] UNIQUE NONCLUSTERED ([Id])
) ON [PRIMARY]
GO

CREATE TABLE [dbo].[Table2]
(
    [PrimaryKey] [bigint] IDENTITY(1,1) NOT NULL,
    [Table1PrimaryKey] [bigint] NOT NULL,
    [Value] [nvarchar](255) NOT NULL,
    
    CONSTRAINT [PK_Table2] PRIMARY KEY CLUSTERED ([PrimaryKey]),
    CONSTRAINT [FK_Table2_Table1] FOREIGN KEY([Table1PrimaryKey])
        REFERENCES [dbo].[Table1] ([PrimaryKey])
        ON UPDATE CASCADE
        ON DELETE CASCADE
) ON [PRIMARY]
GO


次に、SMO を使ってデータベースを管理するクラスを作成。

using System;
using System.IO;
using System.Reflection;
using Microsoft.SqlServer.Management.Smo;

namespace Hoge.Databases
{
    /// <summary>
    /// Hoge データベースを管理します。
    /// </summary>
    public sealed class HogeDatabase
    {
        #region Static Members

        /// <summary>
        /// Hoge データベースに含めるテーブルを生成するための SQL クエリーファイルのリソースパスを取得します。
        /// </summary>
        private const string CreateScriptResourcePath = "Hoge.Databases.Create.SQL";

        /// <summary>
        /// 指定したリソースパスに含まれるリソースを文字列として読み出します。
        /// </summary>
        /// <param name="resourcePath">リソースパス。</param>
        /// <returns></returns>
        static private string ReadResourceText(string resourcePath)
        {
            Assembly thisAssembly = Assembly.GetExecutingAssembly();
            using (Stream resourceStream = thisAssembly.GetManifestResourceStream(resourcePath))
            using (StreamReader resourceReader = new StreamReader(resourceStream))
            {
                return resourceReader.ReadToEnd();
            }
        }

        #endregion

        #region Constructors

        /// <summary>
        /// HogeDatabase クラスの新しいインスタンスを初期化します。
        /// </summary>
        /// <param name="server">SQL Server のインスタンス名。</param>
        /// <param name="database">データベース名。</param>
        public HogeDatabase(string instanceName, string databaseName)
        {
            this._server = new Server(instanceName);
            this._database = new Database(this._server, databaseName);
        }

        #endregion

        #region Fields

        /// <summary>
        /// SQL Server のインスタンスの管理オブジェクトを取得します。
        /// </summary>
        private readonly Server _server;

        /// <summary>
        /// データベースの管理オブジェクトを取得します。
        /// </summary>
        private readonly Database _database;

        #endregion

        #region Properties

        /// <summary>
        /// Hoge データベースが存在するかどうかを示す値を取得します。
        /// </summary>
        /// <returns>Hoge データベースが存在するかどうかを示す値。</returns>
        public bool Exists
        {
            get
            {
                this._database.Refresh();
                return (this._database.State == SqlSmoState.Existing);
            }
        }

        #endregion

        #region Methods

        /// <summary>
        /// Hoge データベース、各テーブルを作成し、ASPNET アカウントをログイン登録します。
        /// </summary>
        /// <exception cref="InvalidOperationException">データベースは既に存在します。</exception>
        public void Create()
        {
            this.CreateDatabase();
            this.CreateTables();
            this.CreateLoginForAspnet();
        }

        /// <summary>
        /// Hoge データベースを作成します。
        /// </summary>
        /// <exception cref="InvalidOperationException">データベースは既に存在します。</exception>
        public void CreateDatabase()
        {
            if (this.Exists)
            {
                throw new InvalidOperationException("データベースは既に存在します。");
            }
            this._database.Refresh();
            this._database.Create();
        }

        /// <summary>
        /// Hoge データベースの各テーブルを作成します。
        /// テーブルが既に存在する場合は、テーブルを削除してから作成を行います。
        /// </summary>
        /// <exception cref="InvalidOperationException">データベースは存在しません。</exception>
        public void CreateTables()
        {
            if (!this.Exists)
            {
                throw new InvalidOperationException("データベースは存在しません。");
            }
            string createScript = ReadResourceText(CreateScriptResourcePath);
            this._database.ExecuteNonQuery(createScript);
        }

        /// <summary>
        /// 結果を返さない SQL コマンドを実行します。
        /// </summary>
        /// <param name="sqlCommand">SQL コマンド。</param>
        /// <exception cref="InvalidOperationException">データベースは存在しません。</exception>
        public void ExecuteNonQuery(string sqlCommand)
        {
            if (!this.Exists)
            {
                throw new InvalidOperationException("データベースは存在しません。");
            }
            this._database.ExecuteNonQuery(sqlCommand);
        }

        /// <summary>
        /// Hoge データベースを削除します。
        /// </summary>
        /// <exception cref="InvalidOperationException">データベースは存在しません。</exception>
        public void Drop()
        {
            if (!this.Exists)
            {
                throw new InvalidOperationException("データベースは存在しません。");
            }
            this._server.Refresh();
            this._server.KillDatabase(this._database.Name);
            System.Data.SqlClient.SqlConnection.ClearAllPools();
        }

        /// <summary>
        /// IIS 5.1 上に配置された ASP.NET アプリケーションから DB にアクセス (R/W) できるよう、
        /// ASPNET アカウントをログイン登録します。
        /// </summary>
        public void CreateLoginForAspnet()
        {
            if (!this.Exists)
            {
                throw new InvalidOperationException("データベースは存在しません。");
            }

            string name = string.Format(@"{0}\{1}", Environment.MachineName, "aspnet");
            this._server.Refresh();
            if (!this._server.Logins.Contains(name))
            {
                Login login = new Login(this._server, name);
                login.LoginType = LoginType.WindowsUser;
                login.Create();
            }

            this._database.Refresh();
            User user = new User(this._database, name);
            if (!this._database.Users.Contains(name))
            {
                user.Login = name;
                user.Create();
            }
            if (!user.IsMember("db_datareader"))
            {
                user.AddToRole("db_datareader");
            }
            if (!user.IsMember("db_datawriter"))
            {
                user.AddToRole("db_datawriter");
            }
        }

        #endregion
    }
}




ユニットテストクラスでの使用例
private const string InstanceName = @"localhost\sqlexpress";
private const string DatabaseName = "HogeDatabase_Test";

private HogeDatabase _testDatabase;

[TestFixtureSetUp]
public void CreateDatabase()
{
    this._testDatabase = new HogeDatabase(InstanceName, DatabaseName);
    if (this._testDatabase.Exists)
    {
        this._testDatabase.Drop();
    }
    this._testDatabase.Create();
}

[SetUp]
public void CreateTables()
{
    this._testDatabase.CreateTables();
}

[TestFixtureTearDown]
public void DropDatabase()
{
    if (this._testDatabase.Exists)
    {
        this._testDatabase.Drop();
    }
}

private void InsertTable1(string id, string name)
{
    const string insertQueryFormat = "INSERT INTO [dbo].[Table1] ([Id], [Name]) VALUES ('{0}', '{1}')";
    string insertQuery = string.Format(insertQueryFormat, id.Replace("'", "''"), name.Replace("'", "''"));

    this._testDatabase.ExecuteNonQuery(insertQuery);
}

[Test]
public void テスト1()
{
    for (int i = 0; i < 3; i++)
    {
        InsertTable1(i.ToString(), string.Format("Test {0:000}", i));
    }

    // テストコード
}


// 2009/01/14
ユニットテストの SetUp と TearDown で DB の作成・削除を実行すると、2 度目以降の DB 作成に失敗してしまうため、DB の作成・削除は TestFixtureSetUp と TestTearDown で行い、テーブルの再作成だけ SetUp で行うように、HogeDatabase クラスとユニットテストを変更。
HogeDatabase クラスの実装方法も全体的に変更。
ついでに ASPNET アカウントの R/W 権限を登録するメソッドも追加。(IIS 5.1 のみ正常に機能する)

// 2009/02/02
テストケースが複数あって DB の作成・削除が2度行われる場合、SqlConnection のプールが残ってしまう関係でエラーが発生する。そのため、DB の削除時に ClearAllPools メソッドを呼び出すよう修正。
参考 : DBバックアップ→DBリストア→テーブル内容参照時にException - Insider.NET
スポンサーサイト



2008/12/18 12:57
こんなクラスがあったとします。
public class Hoge
{
    public Fuga F = new Fuga();
}

public class Fuga
{
    public int V = 0;
}


更にこんなクラスがあったとします。
public class Foo
{
    public Piyo(IBar bar)
    {
        this._bar = bar;
    }
    private _bar;
    public void AAA()
    {
        Hoge h = new Hoge();
        h.F.V = 10;
        this._bar.BBB(h);
    }
}

public interface IBar
{
    void BBB(Hoge h);
}


Foo.AAA メソッドは IBar.BBB メソッドに h (Hoge オブジェクト) を渡しています。
この時、h.F.V に 10 を設定しています。

このことを、NUnit の DynamicMock を使って検証するには、次のような方法が取れます。

using NUnit.Framework;
using NUnit.Framework.Constraints;
using NUnit.Framework.SyntaxHelpers;
using NUnit.Mocks;

[TestFixture]
public class TestFixture1
{
    [Test]
    public void Test1()
    {
        DynamicMock barMockery = new DynamicMock(typeof(IBar));
        Foo foo = new Foo((IBar)barMockery.MockInstance);

        barMockery.Strict = true;
        // BBB メソッドが受け取った Hoge オブジェクトの F プロパティ値の V プロパティ値が 10 であることを期待。
        barMockery.Expect("BBB",
            new PropertyConstraint("F",
                new PropertyConstraint("V",
                    Is.EqualTo(10))));
        foo.AAA();
        barMockery.Verify();
    }
}


Has.Property メソッドを使用するのではなく、PropertyConstraint オブジェクトを直接生成するのがポイントです。

DynamicMock.Expect メソッド (の第2引数) は Constraint が渡されることも想定した作りになっていますが、Has.Property メソッド (の第2引数) はこれを想定した作りにはなっていません。
なので、xxx.yyy.zzz というように、プロパティ値のプロパティ値を検証するには Has.Property メソッドが使えないわけです。
2008/09/04 12:54
僕はユニットテストコードをテスト対象プロジェクトとは別のプロジェクトに用意する派だ。
理由は、同一プロジェクトに格納するとリリース時にそれらを除外しなければならないから。
でも一回そういうスクリプト書いてしまえば使い回しが利くだろうし、これ以外に大した理由はない。

しかし、bleis-tift さんは、僕とは違い同一プロジェクトに含める派で、除外するための手間を逆手に取っている。
C#でのテスト(1) テスト用プロジェクト(?) - 予定は未定Blog版
なるほど、確かにそういう考え方もできる。
また、同一プロジェクトに含む場合、テスト対象とテストの距離が近くなるなど、他にもメリットがあるのだと思う。


所で、最近僕はそもそも同一プロジェクトに含んだとしてもテストコードを除外する必要はないのではないかと疑問に感じている。
テストコードが含まれていることで何かしらの不都合が生じることなんてないのではなかろうか。
強いて言うならアセンブリのファイルサイズが若干大きくなることくらいか、と。

ここで問題となるのが (テストコード除外による) ビルドツールの強制というメリットがなくなるという点。
一見するとデメリットだ。
しかし、テストコードを除外することに本質的な意味がないのであれば、ビルドツールの強制のためだけにテストコード除外というプロセスを組み込むことになってしまう。
つまり、突き詰めればそれはビルドツールを使えと単に指示することと変りないことになる。テストコードを除外する必要がないのだから。


テストコードが含まれていても不都合が本当に生じないのかどうかはわからない。実際に試してみる必要もありそうだ。
2008/06/12 00:03

諸君、私はテスト駆動開発が好きだ
諸君、私はテスト駆動開発が好きだ
諸君、私はテスト駆動開発が大好きだ

レッドが好きだ
グリーンが好きだ
リファクタリングが好きだ

アジャイルプロジェクトで
ペアプログラミングで

この地上に存在するありとあらゆるテスト駆動開発が大好きだ

ユニットテストがもたらす安心感が好きだ
リファクタリングを始める時など心がおどる

シンプルなコードが好きだ
リファクタリングによって洗練されていく様など胸がすくような気持ちだった

設計が常に成長していく様が好きだ
責任が適切に分担されていく様子など感動すらおぼえる

グリーンになると思い込んでいたのにレッドだった時などもうたまらない
いつでも一瞬で全てのコードがテストできるのは最高だ

気づきをもたらしてくれるレッドが好きだ
レッドを省略して実装を進めていく人はとてもとても悲しいものだ

リファクタリングの徹底が好きだ
余計な事だと馬鹿にされるのは屈辱の極みだ

諸君 私はテスト駆動開発を 楽園の守護者の様なテスト駆動開発を望んでいる
諸君 私に付き従うテスト駆動開発好きの諸君 君たちは一体何を望んでいる?
更なるテスト駆動開発を望むか 
糞の様なテスト駆動開発を望むか?
バグの手から私たちを守ってくれる女神のようなテスト駆動開発を望むか?


「テスト駆動開発!!」 「テスト駆動開発!!」 「テスト駆動開発!!」


よろしい ならばテスト駆動開発だ

だが、古い開発プロセスが染みついた場所で変化を拒む連中に耐え続けて来た我々には
ただのテスト駆動開発ではもはや足りない!!
大テスト駆動開発を!! 一心不乱の大テスト駆動開発を!!

我々はわずかに小数
ウォーターフォール派に比べれば物の数ではない
だが諸君は一騎当千の偉大な習慣を身に付けし者だと私は信じている
ならば我らは諸君と私で総兵力100万と1人の救世主の集団となる
我らを忘却の彼方へと追いやり、悪しき習慣にしがみついている奴等を叩きのめそう
髪の毛をつかんで引きずり下ろし 眼(まなこ)をあけて思い出させよう

連中に向上心を思い出させてやる
連中に勇気を思い出させてやる
テスト駆動開発には奴らの哲学では思いもよらない楽しさがある事を思い出させてやる
1000人の偉大な習慣を身に付けし者の集団で 世界を偉大な習慣で埋め尽くしてやる

目標 プログラムの楽しさを忘れてしまった者達

Be Agile 作戦 状況を開始せよ

逝くぞ 諸君



C# でも PowerShell でもなく TDD で挑戦 (?) してみました。
(一部過激な表現が含まれていますが、ネタですのでご勘弁を)

ネタ元 : 諸君、私は ECMAScript が好きだ - IT戦記
ジェネレータ : 「諸君、私はほにゃららが好きだ」ジェネレータ
2007/07/21 21:55
テスト駆動とかXPとかアジャイルとか、興味はあるけどまだ実践したことはありません。
顧客常駐なんて、ウチじゃさすがに無理だと思いますが、テスト駆動は取り入れようと思えば取り入れられると睨んでます。

.NET開発ならNUnitとかNMockとかTestDriven.NETとかを使用する、ということくらいは知ってますが、インストールしたっきり全然使ってなかったりします^^;
で、次の案件ではテスト駆動開発を取り入れようと密かに企んでいたのですが、craftsmanさん
オブジェクト思考: テスト駆動開発やユニットテストを定着させるには という記事を読んで、僕はテスト駆動開発についてのしっかりとした知識を持っておらず、とても危険だったということを思い知らされました。
だって、「構成管理システムによってテスト実行を自動化させる」 なんて事、全く知らなかったんです。普通に手動でNUnitを実行させればいいとばかり思ってました。
こりゃ、取り入れる前にしっかり勉強しとかないと。。。せっかく新しいことをやっても、知識不足のせいで失敗に終わったら台無しですからね。