Spiga

对ASP.NET MVC项目中的视图做单元测试

2009-02-25 01:01 by Jeffrey Zhao, 11246 visits, 收藏, 编辑

关于视图的单元测试

说到ASP.NET MVC,我们似乎始终都在关注对于Controller的测试——虽然Stephen Walther也写过如何脱离Web Server对View进行单元测试,但是他的方法可看而不可用。复杂的构造和预备,以及对生成的HTML字符串作判断——这真是在对视图做单元测试吗?仔细分析他的代码可以发现,这其实是在对ViewEngine做单元测试。而且,如果真要对ViewEngine做单元测试,也不应该像他那样依赖外部文件。在我看来,他的做法什么都不是……似乎美观,似乎能博得一些“掌声”,但是这个掌声是来自于他的解决方案,还是大家一时的冲动呢?

如果要对视图做单元测试,还是要将内容呈现在浏览器中才行。在对网页做单元测试时,我们一般会使用WatiN等工具操作浏览器,打开页面,再对其DOM元素结构及内容作断言。不过……这是单元测试吗?可惜这只能算是一种回归测试或用户验收测试。因为,我们在打开一个页面的时候,从表现层到业务逻辑再到数据访问,应用程序的每个部件都在忙碌着。而单元测试讲究的是“分离”,分离一切关注,分离一切依赖。因为分离,我们才能准确定位错误;因为分离,我们才能在测试中使用我们准备好的数据。

既然要分离,我们就必须遵循一定的使用规范。在《ASP.NET MVC单元测试最佳实践》中我提到,在View中只能使用ViewData中的数据,而不该依赖其他内容(包括HttpContext)。这样我们就可以自行构造ViewData并注入一个视图对象中。事实上,这个约定在ASP.NET MVC自带的项目模板中就被破坏了。请看Views\Shared\LogOnUserControl.ascx,其中通过this.User来查看当前用户的登陆状态。这是个定义在传统Page对象上的属性,从当前HttpContext上直接获取。如果使用这种方式,我们在单元测试时就难以“模拟”当前用户的登陆状态,进而难以使测试覆盖到测试的各种情况了。

Lightweight Test Automation Framework

在这里,老赵推荐使用ASP.NET Team提供的Lightweight Test Automation Framework(下文称之为LTAF)作为测试工具,它目前已经在CodePlex上更新至Feb Update版本。这个框架的作用与WatiN和Selenium类似,可操作浏览器对应用程序编写回归测试。虽然在某些方面(例如DOM元素的选取)不如“竞争对手”,但是LTAF自有其独到之处:

  • 由于直接在浏览器中运行,它天生便支持现有的——以及未来可能出现的任意浏览器。
  • 由于直接部署在被测试的网站中,因此测试代码和网站页面是在同一个进程中。

第一点优势自不必说,而第二点更是关键。试想WatiN和Selenium,都是通过编写代码在浏览器中打开页面。这意味着我们的在测试代码和被测试的网页分别在不同的进程中。在这个前提下,如果我们要将测试代码中定义的数据传递给被测试的网页(也就是视图对象),我们就必须进行跨进程的通信。而无论怎么实现,都逃不过“序列化”一途,这无疑增加了复杂度。而使用LTAF之后,这个问题瞬间烟消云散了,因为我们可以直接在内存中“传递”测试数据,一切都只是个引用而已。

不过任何事物都具有两面性,LTAF也有一些难以天生的,而且是永远无法弥补的缺点。例如:

  • 由于LTAF将待测试的页面放置在Frame中,因此该页面上的window.top等基于浏览器frame结构的属性会被改变。
  • 由于LTAF的本质是使用JavaScript来操作DOM,这意味着任何会阻塞程序进行的操作(例如alert)都不能使用,否则将阻塞整个测试过程。

不过幸运的是,这两点都不回成为严重的问题。对于第一种,我们只需要编写一个自定的getTop方法来替换直接访问windows.top的做法即可。而第二种情况——老赵从来不喜欢alert或confirm这种“纯浏览器功能”,因为它们会带来很差的用户体验,更何况现在的JavaScript类库/框架都能很轻松的做出这种效果,您觉得呢?

LTAF的具体使用方式可参考其Release Note。令人奇怪的是,老赵发现直接在项目中使用LTAF会有一些小问题(不过它的示例为什么就一切正常呢?),因此进行了一些细微的修改。请注意~\UnitView\DriverPage.aspx文件尾部的一些JavaScript代码。

UnitView的使用

于是老赵编写了一个组件UnitView,方便我们构造一个单元测试时所需的数据。有了数据,便能够直接将视图在浏览器中加以呈现了。例如:

[WebTestClass]
public class HomeTests
{
    [WebTestMethod]
    public void LoggedOnIndexTest()
    {
        var data = new TestViewData<IndexModel>
        {
            ControllerName = "Home",
            ActionName = "Index",
            Model = new IndexModel
            {
                Message = "Welcome guys!",
                Identity = new UserIdentity
                {
                    IsAuthenticated = true,
                    Name = "Jeffrey Zhao"
                }
            }
        };

        HtmlPage page = new HtmlPage(TestViewData.GenerateHostUrl(data));

        // Assert title
        Assert.AreEqual("Home Page", page.Elements.Find("title", 0).GetInnerText());

        // Assert head element
        var mainContent = page.Elements.Find("main");
        var head2 = mainContent.ChildElements.FindAll("h2").Single();
        Assert.AreEqual(data.Model.Message, head2.GetInnerText(), "Message should be displayed.");

        var loginTabInnerText = page.Elements.Find("logindisplay").GetInnerTextRecursively();
        Assert.IsTrue(loginTabInnerText.Contains("Welcome"), "'Welcome' missed.");
        Assert.IsTrue(loginTabInnerText.Contains(data.Model.Identity.Name), "Login name missed.");
    }
}

自然,Web Server是不可或缺的。幸运的是,分离让我们的视图只会涉及最简单的测试数据,这样VS自带的简单Web Server就足够了。在上面的代码中,我们直接构造了强类型的TestViewData对象,它包含呈现一个视图所需要的所有数据:

  • Cotroller和Action名称。从理论上说,由不同的Controller和Action进入同样的视图可能会得到不同的结果。
  • View和Master名称。如果省略,则表明将使用默认的视图,即通过Controller和Action的值来确定。
  • ViewData和Model。

TestViewData.GenerateHostUrl方法会把data保存起来,并返回一个URL。访问该URL便能够得到对应的视图内容。

如果您想使用UnitView,可以从上面的链接中下载UnitView的源代码和示例在本机进行尝试。使用UnitView时主要有以下几个注意点:

  1. 将Tests项目的输出路径指向被测试网站的bin目录,这样既可以在运行时得到正确的程序集,又不必为网站添加多余的引用。
  2. 将~\UnitView目录复制到您的网站根目录下(在发布网站时,请剔除该目录)。如果想使用其它目录,请关注接下来UnitView实现分析。
  3. 编辑~\UnitView\Web.config文件,将MvcApp.Tests.dll修改为您自己的包含测试代码的程序集。

UnitView实现分析

UnitView组件非常简单,简单地几乎不值一提。TestViewData类型包含了测试需要的所有数据,而TestViewData<TModel>继承了TestViewData,提供了强类型的Model属性访问方式。它们就不作分析了。

此外,TestViewData还有一些静态方法:

public class TestViewData
{
    static TestViewData()
    {
        PersistentProvider = new InProcPersistentProvider();
    }

    public static IPersistentProvider PersistentProvider { get; set; }

    public static string GenerateHostUrl(TestViewData data)
    {
        var key = PersistentProvider.Save(data);
        return ViewHostHandlerUrl + "?key=" + HttpUtility.UrlEncode(key);
    }

    private static string ViewHostHandlerUrl
    {
        get
        {
            return ConfigurationManager.AppSettings["UnitView_ViewHostHandlerUrl"]
                ?? "/UnitView/ViewHostHandler.ashx";
        }
    }

    internal static TestViewData Load(string key)
    {
        return PersistentProvider.Load(key);
    }
    ...
}

GenerateHostUrl方法将委托PersistentProvider保存对象,并得到一个key。这个key将拼接在ViewHostHandlerUrl属性上,这便是被测试的路径。从代码中可以看出,如果您不想使用默认的测试路径,只需在web.config的AppSettings节点中添加一个目标地址即可。

PersistentProvider属性为IPersistentProvider接口类型,其中定义了Save/Load/Remove三个方法。IPersistentProvider在项目中只有一个实现:InProcPersistentProvider,它会将TestViewData存放在内存中的一个字典里。这个实现已经足够让UnitView结合LTAF运行(LTAF的同进程特性起到了关键的作用)。不过,如果您还是希望使用WatiN等独立进程的测试工具,就必须实现自己的IPersistentProvider类型。例如您可以实现一个FilePersistentProvider,将TestViewData序列化至一个外部文件中,这样就可以在合适的时候将它取回了。

另一个较为关键的类型是UnitView.Engine.ViewHostHandler:

public class ViewHostHandler : IHttpHandler
{
    private HttpContext Context { get; set; }

    public void ProcessRequest(HttpContext context)
    {
        this.Context = context;

        ControllerContext controllerContext = new ControllerContext(
            new HttpContextWrapper(context),
            this.Data.RouteData,
            new MockController());

        new ViewResult
        {
            MasterName = this.Data.MasterName,
            ViewName = this.Data.ViewName,
            TempData = this.Data.TempData,
            ViewData = this.Data.ViewData,
        }.ExecuteResult(controllerContext);
    }

    private string Key
    {
        get
        {
            string key = this.Context.Request.QueryString["key"];
            if (String.IsNullOrEmpty(key))
            {
                throw new ArgumentNullException("key");
            }

            return key;
        }
    }

    private TestViewData m_data;
    private TestViewData Data
    {
        get
        {
            if (this.m_data == null)
            {
                this.m_data = TestViewData.Load(this.Key);
                if (this.m_data == null)
                {
                    throw new ArgumentNullException("Cannot retrieve the data.");
                }
            }

            return this.m_data;
        }
    }

    public bool IsReusable { get { return false; } }
}

首先,在ProcessRequest方法会取回TestViewData,并根据这些数据构造一个ViewResult对象,最后执行它的ExecuteResult方法来输出视图内容。由于ExecuteRequest方法的需要,我们还必须构造一个ControllerContext对象,也就意味着我们还必须提供一个Controller对象和HttpContext的封装。从代码中可以看出,我们这里使用了最简单的数据。由于视图遵守“约定”,它只会从ViewData中获取数据,所以无论Controller或HttpContext是什么值都已经无关紧要了。

您可能会想,为什么会有这样的“约定”,不让视图从HttpContext对象中获取数据呢?Mock一个HttpContext对象也不是那么困难(这里要感谢各种强大的Mock框架)啊。可惜,Mock后的HttpContext很难进行序列化,这样就几乎杜绝了跨进程通信的可能,这对于使用WatiN和Selenium进行测试的朋友们无疑是一种灾难。权衡之下,老赵决定放弃对HttpContext的支持。

 

注1:目前UnitView基于ASP.NET MVC RC构建,当RTM发布后我会进行必要的更新。请关注老赵这篇文章和托管在MSDN Code Gallery上的代码(http://code.msdn.microsoft.com/UnitView)。

注2:在《ASP.NET MVC单元测试最佳实践》中我也包含了UnitView组件,实现略有不同——请以本篇文章为主。

Add your comment

46 条回复

  1. #1楼[楼主] Jeffrey Zhao      2009-02-24 01:29
    怎么英文都变小写了?好丑。
     回复 引用 查看   
  2. #2楼 Anders Cui      2009-02-24 02:03
    @Jeffrey Zhao
    没关系,我看了一下,文章内容没问题,应该很快就会好的:-}
     回复 引用 查看   
  3. #3楼 Anders Liu      2009-02-24 06:59
    打死我也不信只用小写字母就能写程序……
     回复 引用 查看   
  4. #4楼[楼主] Jeffrey Zhao      2009-02-24 07:59
    --引用--------------------------------------------------
    Anders Cui: @Jeffrey Zhao
    没关系,我看了一下,文章内容没问题,应该很快就会好的:-}
    --------------------------------------------------------
    嗯,是样式,不是内容的问题
     回复 引用 查看   
  5. #5楼[楼主] Jeffrey Zhao      2009-02-24 08:00
    --引用--------------------------------------------------
    Anders Liu: 打死我也不信只用小写字母就能写程序……
    --------------------------------------------------------
    语法上总归没有问题的,恩恩
     回复 引用 查看   
  6. #6楼 ssseewew[未注册用户]2009-02-25 00:24
    写得不错!谢谢了! (本人站点http://www.0663bx.com/yellow)
     回复 引用   
  7. #7楼 Ryan Gene      2009-02-25 00:26
    .NET MVC现在貌似有点火,但个人觉得.net真的需要再加mvc这个概念吗?因为.net天生就有code behind的这个东东,以前将ror(ror的mvc的确不错,不过后来没深入下去)和.net做对比,感觉.net的aspx就是view,aspx.cs就是controller,一些datasource之类的就是model。

    随便说说,可能是我对mvc理解不深,呵呵
     回复 引用 查看   
  8. #8楼 T2噬菌体      2009-02-25 00:31
    先顶再看
     回复 引用 查看   
  9. #9楼[楼主] Jeffrey Zhao      2009-02-25 00:57
    @Ryan Gene
    组件之间交互方式是不同的。
    aspx/aspx.cs组成了Page Controller模式,而现在的mvc框架其实是Front Controller模式。
    其实在webform里使用良好的分离也可以实现很好的view/controller特性,可惜大家多在乱用。
    个人认为死扣“MVC”模式意义不大,Web平台上实现真正的MVC本就难以作到,所以我一直认为唯一不变的是“分离”,而不是到底mvc不mvc。
    很明显Web Form和mvc框架两种东西是不同的驱动方式,选择一多总是好事……当然前提是思路要清晰。
     回复 引用 查看   
  10. #10楼 Ryan Gene2009-02-25 08:50
    谢谢你这么晚了还回复,的确,滥用的确是一个问题,其中一个原因可能是微软提的让web开发像winform那样简单吧,导致很多.net程序员对http,html,css了解不够。补充点我现在的做法,首先尽量把页面切分成user control,一方面为重用,另一方面化烦为简,每个user control实现必须的接口,例如savedata,loaddata等,aspx.cs代码组织好object或object数组,提供给页面上的user controls 进行自我绑定。当然实现可以是多样的,这是编程的乐趣之一,也是体力劳动和脑力劳动的区别,呵呵
     回复 引用   
  11. #11楼 RawMan      2009-02-25 08:58
    楼主很强,很伟大
     回复 引用 查看   
  12. #12楼[楼主] Jeffrey Zhao      2009-02-25 09:14
    @Ryan Gene
    嗯,这是个好习惯,构建自己的编程模型/约定等等。
     回复 引用 查看   
  13. #13楼 假正经哥哥      2009-02-25 09:24
    意义不大。。
     回复 引用 查看   
  14. #14楼[楼主] Jeffrey Zhao      2009-02-25 09:31
    --引用--------------------------------------------------
    假正经哥哥: 意义不大。。
    --------------------------------------------------------
    为啥?
     回复 引用 查看   
  15. #15楼 重典      2009-02-25 09:50
    5555555
    快枪赵真是快

    HttpContext。。的问题
    我觉得在View之下只会读取,一般不写
    我仿了一下MonoRail的方法将
    QueryString/Form/Session/RouteData、ServerVariables都在OnActionExecuting时堆到ViewData里了至于其它的方法可以重写或调用静态方法
     回复 引用 查看   
  16. #16楼[楼主] Jeffrey Zhao      2009-02-25 10:00
    @重典
    View读取HttpContext也不好啊。
    至于推到ViewData里……我还是认为要尽可能使用强类型,所以对于ViewData的感觉不是很好。
     回复 引用 查看   
  17. #17楼 重典      2009-02-25 10:08
    @Jeffrey Zhao
    强类型是可以直接以编译错误找出问题所在,不过如果传递的类型比较多也繁杂的话,那样只能单独建立Model类来实现了
     回复 引用 查看   
  18. #18楼[楼主] Jeffrey Zhao      2009-02-25 10:26
    @重典
    对,我建议每个View都使用强类型的Model。
     回复 引用 查看   
  19. #19楼 重典      2009-02-25 10:51
    @Jeffrey Zhao
    请注意~\UnitView\Default.aspx文件尾部的一些JavaScript代码。

    -》应为

    请注意~\UnitView\DriverPage.aspx文件尾部的一些JavaScript代码。

     回复 引用 查看   
  20. #20楼 T2噬菌体      2009-02-25 10:54
    不错,学到不少东西。
    现在我调试视图都是得现在浏览器里运行。。。
     回复 引用 查看   
  21. #21楼[楼主] Jeffrey Zhao      2009-02-25 11:09
    @重典
    我错了……
     回复 引用 查看   
  22. #22楼 海洋——海纳百川,有容乃大.      2009-02-25 11:14
    老赵,看了你的留言,看来你的FANS 很多啊!我也留一个。
    .NET 技术大会上,你好像讲过这个,不过还是非常感谢分享。
     回复 引用 查看   
  23. #23楼 xjb      2009-02-25 11:21
    不错,关注ing
     回复 引用 查看   
  24. #24楼[楼主] Jeffrey Zhao      2009-02-25 11:30
    --引用--------------------------------------------------
    海洋——海纳百川,有容乃大.: 老赵,看了你的留言,看来你的FANS 很多啊!我也留一个。
    .NET 技术大会上,你好像讲过这个,不过还是非常感谢分享。
    --------------------------------------------------------
    我讲过“要对View做单元测试”,没有说过该怎么测试,以及UnitView是怎么做的。
     回复 引用 查看   
  25. #25楼 T2噬菌体      2009-02-25 11:32
    非常同意老赵“视图仅应依赖ViewData”这一观点。不过,个人还是觉得视图应该做成模板形式,不要承担逻辑
     回复 引用 查看   
  26. #26楼[楼主] Jeffrey Zhao      2009-02-25 11:38
    @T2噬菌体
    aspx本来就是一种模板形式。
    说到逻辑——模板中肯定也是会包含逻辑的,呈现上的逻辑。例如,试图会根据“是否有上一页”来确定是不是要显示这个链接。
    当然确定“是否有上一页”是Controller/Model的职责,视图只负责读取“是否有上一页”这一情况。
     回复 引用 查看   
  27. #27楼 假正经哥哥      2009-02-25 12:03
    当我在公司推行MVC的时候,连Controller的单元测试推行都不被接受(认为成本太高),何况是View的测试呢。
    况且View测试断言并非是在哪里可以拿到什么数据,或这显示什么数据,
    对页面问题,往往是:如下
    1:表格的宽度顶出的边框。。
    2:下拉图层没有遮住select(IE6)
    3:...............

    还是需要眼睛去看。。。光靠断言不行啊。。。
    但是对Controller的测试我倒是认为非常有必要。。。。怒啊,环境,领导。。!
     回复 引用 查看   
  28. #28楼[楼主] Jeffrey Zhao      2009-02-25 12:09
    @假正经哥哥
    对View做单元测试,测试的是“数据”和“结构”和“行为”;而眼睛看的是“样式”,两者都不能省。如果没有自动的测试,测试人员还必须在页面上查看数十个链接是否正确。
    还例如,现在可以省下检查客户端输入校验功能是否正常的工作,只需要用自动测试的方式:“输入错误信息,点击提交按钮,检查错误提示是否出现”就可以了。
    至于你说单元测试的功效不能被接受,那么我的看法是,你的团队不适合用MVC框架,以及……不适合写长期的项目……
     回复 引用 查看   
  29. #29楼 妖居      2009-02-25 12:46
    日发一篇啊。老赵辛苦了。开头对Stephen的评论深有同感。我当时看得时候也感觉他那篇文章写得不伦不类。
     回复 引用 查看   
  30. #30楼[楼主] Jeffrey Zhao      2009-02-25 12:48
    --引用--------------------------------------------------
    妖居: 日发一篇啊。老赵辛苦了。开头对Stephen的评论深有同感。我当时看得时候也感觉他那篇文章写得不伦不类。
    --------------------------------------------------------
    嗯嗯,我们要擦亮眼睛,一味跟风是不好滴……
     回复 引用 查看   
  31. #31楼 零点      2009-02-25 14:10
    每次读老赵的文章都很受益哦!
     回复 引用 查看   
  32. #32楼 哦ioi[未注册用户]2009-02-25 17:31
    老赵:
    有空翻译基本国外经典技术书籍吧,看看这位仁兄翻译的书籍:

    下面是我在其译章里随意(绝对是鼠标随意找的句子),你们看看,这样的书籍,呵呵呵-----------------一本《C#3.0 in a nutshell》多好的书呀,却被。。。
    (我强烈呼吁:非行内人不要翻译技术书,至少你好好读读行业术语)

    extend interface居然翻译成 拓展接口?
    一个适配器包裹了一个流,跟修饰模式一样
    流必须在使用后关闭或者处置掉
    第12章 处置与回收
    一些对象需要特定的,具有粉碎性的代码来释放资源,
    并且描述C#终止器(finalizers)

    就算意思通,你也应该润饰一下呀
     回复 引用   
  33. #33楼[楼主] Jeffrey Zhao      2009-02-25 17:34
    @哦ioi
    没空啊,好多东西要写要学
     回复 引用 查看   
  34. #34楼 奥地利[未注册用户]2009-02-25 17:40
    透露一下最近学习内容?呵呵
     回复 引用   
  35. #35楼 ads的[未注册用户]2009-02-25 17:54
    --引用--------------------------------------------------
    奥地利: 透露一下最近学习内容?呵呵
    --------------------------------------------------------
    感兴趣
     回复 引用   
  36. #36楼[楼主] Jeffrey Zhao      2009-02-25 17:55
    @奥地利
    大学课本翻出来从头学
     回复 引用 查看   
  37. #37楼 RogerTong      2009-02-25 21:10
    JeffreyZhao换头像了?哈哈,好像还是以前那个靓仔点:)
     回复 引用 查看   
  38. #38楼 airwolf2026      2009-02-25 21:47
    还没有接触ASP.NET MVC的东东
    不过还是能从老赵这篇blog获取知识哈.比如你说的'单元测试'要分离,这个俺以前没有认识到,可能还停留于'单元测试'的肤浅认识吧,另外了解到了几个相关的测试工具,还是第一次了解,感觉比vs里面带的web测试工具更方便.
     回复 引用 查看   
  39. #39楼[楼主] Jeffrey Zhao      2009-02-25 22:42
    @airwolf2026
    这些工具应该算是对VS的补充,功能上没有太大可比性
     回复 引用 查看   
  40. #40楼 老鼠      2009-02-28 09:37
    您好,能否告诉我如何下你的mvc视屏吗?我没有找到!!可以发邮件告诉我吗?weilian_520@126.com 谢谢
     回复 引用 查看   
  41. #41楼 水果阿生      2009-03-01 10:24
    抛开一切不谈,老赵你的瘦身计划很成功。另外,老苏也已经成功瘦下5公斤了。
     回复 引用 查看   
  42. #42楼[楼主] Jeffrey Zhao      2009-03-01 14:42
    @水果阿生
    怎么做到的亚?
     回复 引用 查看   
  43. #43楼 Nick Wang      2009-03-23 23:58
    个人感觉,意义不大。每个页面都这么折腾要花很多功夫,收到的效果却未必有多大,也就是说投入产出比不大。完全可以用其他方式的测试代替。

    另外waitN和selenium似乎都是做acceptance test的,和Unit test还是不一样的吧。

    有太多人把注意力放在controller和view的测试上了,虽然没啥不好,但是model的测试才是最复杂和最重要的啊。只有model正确了,controller和view才有意义,可惜的是这么多人都在追mvc,silverlight这些表现层的东西,却对model不上心啊。(感叹一下)
     回复 引用 查看   
  44. #44楼[楼主] Jeffrey Zhao      2009-03-24 00:03
    @Nick Wang
    1、没有必要每个页面都这么折腾,对于一些客户端功能测试,例如输入验证,有个自动测试可以省很多事情。
    2、WaitN和Selenium也可以做验收测试的,你可以搜以下前两个和UnitTest,可以有一大堆结果,也常用来做UnitTest。
    3、不是不重视Model,而是Domain的单元测试已经不用强调了,从单元测试已开始测试的几乎都是领域模型,类库,框架云云;反之,表现层的测试一直是个麻烦问题,因此现在一直在强调——兄弟不用觉得世道变了,呵呵。
     回复 引用 查看   
  45. #45楼 Nick Wang      2009-03-24 00:18
    @Jeffrey Zhao
    你不是天天都这么晚睡吧?牛人就是不一样啊。

    3. model的单元测试虽然已经有历史了,但是感觉国内或者园子内还不够重视,model的UT还不会写呢就开始写表现层了,这个有点头重脚轻了。不是和你有相反的见解,只是感慨一下。也希望借你的影响力,强调一下,希望大家都能写出高质量的代码,早点回家休息。
     回复 引用 查看   
  46. #46楼[楼主] Jeffrey Zhao      2009-03-24 00:21
    @Nick Wang
    睡的早觉得浪费时间啊……
    我觉得其实倒不是不够重视Model的单元测试,而是不重视单元测试本身,这的确是一个问题……
     回复 引用 查看   
发表评论

昵称: [登录] [注册]

主页:

邮箱:(仅博主可见)

评论内容:

  登录  注册

[使用Ctrl+Enter键快速提交评论]

0 1396718 VttUmxBwZjs=