惟愿终日无丝竹之乱耳,无案牍之劳形。看那平心所至处,皆为仙境。

[C#]像Rails下一样的Unit Testing

上一篇 / 下一篇  2007-09-05 15:19:10 / 个人分类:搞搞技术

原文首发:Ruby中文社区
社区地址:http://www.ruby-lang.org.cn/
转载请保留版权声明并以连接形式指向原文。


用上了Rails后,做单元测试变成很爽;但是上班的时候,用C#做单元测试老是感到不够自在,虽然NUnit也非常好用,但是一旦碰上数据库的依赖项需要测试的时候(例如对Castle.ActiveRecord的Bizness层进行测试)就感到非常的烦琐。怎么办呢?

书上说的是单元测试应该降低对任何项的依赖,但是这个不太现实。没有数据库的支持,我怎么知道数据方面的功能是否正常呢?Mock一个数据库?呵呵,不太现实。在测试的时候数据冲突等问题搞得测试非常的脆弱,简直是弱不经风啊。

我一直想念Rails里的fixtures,夹具。这是一个非常好的东西,对于每个测试方法都保证了相同的环境可供使用,从而降低了测试的不稳定性。但是现在在世界上没有这么好用的东西,没办法自己做吧。

首先,我们建立一个测试基类UnitTestBase。

     public class UnitTestBase
    {
        private string connectionString = null;

        private string path = null;
        private string setupScript = null;
        private string cleanupScript = null;
        private SqlConnection conn;
        private SqlTransaction trans;

        public UnitTestBase()
        {
            //init activerecord
            InitActiveRecord.Init();

            //get path
            path = Path.Combine(ConfigurationManager.AppSettings["test.fixture.path"], GetType().Name.ToLower().Replace("test", "") + ".sql");
            path = path.Replace("~/", AppDomain.CurrentDomain.BaseDirectory + "/");

            //get config options
            string connectionName = ConfigurationManager.AppSettings["test.connection.name"] ?? "Default";
            connectionString = ConfigurationManager.ConnectionStrings[connectionName].ConnectionString;

            if (!File.Exists(path)) return;

            //get sql script
            string all = File.ReadAllText(path);
            string[] tmp = Regex.Split(all, "#{4,}");
            setupScript = tmp[0];
            if (tmp.Length > 1)
                cleanupScript = tmp[1];
        }

        public virtual void Setup()
        {
            if (string.IsNullOrEmpty(setupScript)) return;

            //execute setup script
            conn = new SqlConnection(connectionString);
            conn.Open();

            SqlCommand cmd = new SqlCommand();
            cmd.Connection = conn;
            cmd.CommandText = setupScript;
            cmd.CommandType = CommandType.Text;
            cmd.ExecuteNonQuery();

            trans = conn.BeginTransaction();
        }

        public virtual void Down()
        {
            if (trans != null) trans.Rollback();

            if (!File.Exists(path)) return;
            if (string.IsNullOrEmpty(cleanupScript))
                return;

            //execute cleanscript
            SqlCommand cmd = new SqlCommand(cleanupScript, conn);
            cmd.CommandType = CommandType.Text;
            cmd.ExecuteNonQuery();

            conn.Close();
        }

        public delegate void Handler();
        public static void AssertException(Type exceptionType, Handler method)
        {
            try
            {
                method.Invoke();
                Assert.Fail(string.Format("{0} excepted\nbut wasn't exception.", exceptionType.Name));
            }
            catch (Exception ex)
            {
                if (!ex.GetType().Equals(exceptionType))
                    Assert.Fail(string.Format("{0} excepted\n but was {1}", exceptionType.Name, ex.GetType().Name));
            }
        }
    }

上面的代码很简陋,因为主要是自己在开发过程中使用,并没有打算弄成什么框架之类的,所以简单就简单吧。稍懂些C#的朋友应该都看出来逻辑了。非常简单,在类的构造函数里,我们加载配置项,找到连接字符串,fixture的路径,找到setup的script和cleanup的script,将他们放入变量中;在setup方法里,我们运行setup script并且开如一个事务。在down方法里我们回滚事务,并且执行清除的sqlscript。

注意上面还有一个AssertException的方法,这是用来测试异常的。我十分不喜欢每一个异常都需要用一个ExceptedException的Attribute来测试。这样使得测试方法变得很多个。现在你可以这样使用这个AssertException.

                 AssertException(typeof(NotFoundException), delegate
                {
                    biz.CommentPhoto(content, -1, commentor);
                });


好了,这样就方便多了。

看看app.config.
 <?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <configSections>
        <section name="activerecord" type="Castle.ActiveRecord.Framework.Config.ActiveRecordSectionHandler, Castle.ActiveRecord"/>
    </configSections>

    <appSettings>
        <add key="web.physical.dir" value="F:\Projects\MyProject\src\trunk\MonoRailSite" />
        <add key="web.virtual.dir" value="/" />
        <add key="test.fixture.path" value="~/../../Fixtures"/>
        <add key="test.connection.name" value="Default"/>
    </appSettings>

    <connectionStrings>
        <add name="Default" connectionString="Integrated Security=SSPI;Persist Security Info=False;Initial Catalog=CF;Data Source=.\SQLExpress" />
    </connectionStrings>

    <activerecord isWeb="false">
        <config>
            <add key="hibernate.connection.driver class" value="NHibernate.Driver.SqlClientDriver"/>
            <add key="hibernate.dialect" value="NHibernate.Dialect.MsSql2000Dialect"/>
            <add key="hibernate.connection.provider" value="NHibernate.Connection.DriverConnectionProvider"/>
            <add key="hibernate.connection.connection_string" value="ConnectionString = ${Default}"/>
        </config>
    </activerecord>
</configuration>
因为是使用Castle的,所以里面包含了一些ActiveRecord的配置,不过结合上面的代码,看起来应该非常简单的,以test开始的appSetting才是我们需要的。

下面是sqlscript的一个示例,注意setup和cleanup以4个以上的#号分隔。
 delete from albums;
delete from profiles;
delete from users

set identity_insert users on
insert into users (ID, Name, Email, HasedPassword, Subscribed, CreatedAt, Approved, Enabled) values (1, 'Jerome', 'jerome@chinasoftware.eu', '123456', 0, '2007-08-31 12:00:00.000', 1, 1)
set identity_insert users off
insert into profiles (UserID, FirstName, LastName, Company, PhoneNumber, StreetAddress, City, State, CountryID, PostalCode, LastLogin, AvatarUrl, Signature) values (1, 'Jerome', 'Chen', 'Yardi', '5550586', 'ChuangYeYuan', 'Xiamen', 'Fujian', null, '361009', '2007-08-31 00:00:00.000', null, null)

############################
delete from albums;
delete from profiles;
delete from users

注意:命名规则为测试类的文件名去除test。

我们来看看具体的测试代码:
     [TestFixture]
    public class ProfileTest : UnitTestBase
    {
        Profile profile = null;

        [SetUp]
        public override void  Setup()
        {
            base.Setup();
            profile = Profile.FindFirst();
        }

        [TearDown]
        public void Clean()
        {
            this.Down();
        }

        [Test]
        public void ExtendPropertyTest()
        {
            profile["RealName"] = "Jerome";
            Assert.AreEqual("Jerome", profile["RealName"]);
            profile.Save();

            profile.Refresh();
            Assert.AreEqual("Jerome", profile["RealName"]);
        }
    }
我们需要让测试类继承于我们的基类,并且需要设置setup和teardown的 attribute。这样使得我们的测试类拥有了自动设置数据库环境的能力。在每个测试方法运行前,将进行setup script的调用,在每个方法执行完毕后,又会调用cleanup script的执行。


大体的目录结构如下所示:
root -
        |- fixtures
            |- profile.sql
        |- functional
        |- unit
            |-profileTest.cs

OK,尝试一下C# + Rails测试的感觉吧。你一定会爱上它的。

相关阅读:

TAG: activerecord csharp fixtures unittest

引用 删除 Guest   /   2007-09-06 11:55:14
Ha!
 

评分:0

我来说两句

显示全部

:loveliness: :handshake :victory: :funk: :time: :kiss: :call: :hug: :lol :'( :Q :L ;P :$ :P :o :@ :D :( :)

Open Toolbar