• C#测试 争议TDD(测试驱动开发)


    -----------------------

    绝对原创!版权所有,转发需经过作者同意。

    -----------------------

    在谈到特性的使用场景时,还有一个绝对离不开的就是

    单元测试

    按飞哥的定义,单元测试是开发人员自己用代码实现的测试 。注意这个定义,其核心在于:

    • 主体是“开发人员”,是测试人员。
    • 途径是“通过代码实现”,是通过手工测试。
    • 实质是一种“测试”,是代码调试。

    暂时还有点抽象,同学们记着这个概念,我们先用一个

     

    NUnit项目

    来看一看单元测试长个什么样。

    在solution上右键添加项目,选择Test中的NUnit Test Project,输入项目名称,点击OK:

    Visual Studio直接集成了NUnit说明微软在开源和社区支持的路上确实是一路狂奔,因为NUnit是一个由社区支持的、完全开源的、和微软自己的MSTest Test和Unit Test直接竞争的单元测试框架。微软确实已经从“什么都要自己有”向“借用(不仅是借鉴)乃至大力支持一切优质开源项目”华丽转身。

    新建的单元测试项目包含一个默认的类文件:UnitTest1.cs,其中首先使用了using:

    using NUnit.Framework;
    

    因为NUnit的所有成员(类和方法等)都在NUnit.Framework命名空间之下。

    然后有一个类:

        public class Tests
        {
            [SetUp]
            public void Setup()
            {
            }
    
            [Test]
            public void Test1()
            {
                Assert.Pass();
            }
        }
    

    你发现这个项目和Console Project不同,它没有没有Main()函数作为入口,怎么运行呢?就算我知道它可以由NUnit调用,但NUnit怎么调用呢?这就需要用到 反射 了:NUnit会在整个程序集(项目)中遍历,找到带有特定标签(特性)的类和方法,予以相应的处理。

    注意这个类里面的两个方法都被贴上了特性:

    • SetUp:被标记的方法将会在每一个测试方法被调用前调用
    • Test:被标记的方法会被依次调用

    NUnit是依据特性而不是方法名来确定如何调用这些方法的,所以Tests的类名和其中的方法名都可以修改。

    那么如何启动测试呢?快捷键Ctrl+E+T,或者在VS的菜单栏上,依次:Test-Windows-Test Explore打开测试窗口即可:

    然后在Test1上点击右键,就可以Run(运行)或者Debug(调试)这个测试方法了。

    演示:

     

    测试方法中现在可以使用

    Assert(断言)

    调用各种方法,最常用的是Assert.AreEqual(),比较传入的两个参数:

            [Test]
            public void Test1()
            {
                Assert.AreEqual(5, 3 + 2);
            }
    
            [Test]
            public void Test2()
            {
                Assert.AreEqual(8, 3 + 2);
            }
    

    前面一个参数代表你期望获得的值,后面一个参数代表实际获得的值。如果两个值相等,测试通过;否则会抛出AssertException异常。

    一个方法里可以有多条Assert语句,只有方法里所有Assert语句全部通过,方法才算通过测试。方法通过,用绿色√表示;否则,用红色×标识。

    点击未通过的方法,可以看到其详细信息:

    尤其是StackTrace,是我们定位未通过Assert的有力工具。

     

    当然上面的演示是没有实际作用的,3+2=5这是在测试C#的运算能力呢,^_^。我们要测试的,是我们自己写的代码(通常是方法)。比如,Student类(学生)有一个实例方法Grow(),每调用一次该方法,这个学生的年龄就增长一岁。

    所以我们应该怎么做?先实现这个方法吧……注意,注意,注意!标准(推荐)的做法不是这样的,而应该是:先测试,再开发 。

    啥?一脸懵逼,(黑人问号.jpg

    这就不得不提到大名鼎鼎的:

    TDD

    其全称是Test-Driven Development(测试驱动开发),其核心是:在开发功能代码之前,先编写单元测试用例代码。具体来说,它要求的开发流程是这样的:

    1. 写一个未实现的开发代码。比如定义一个方法,但没有方法实现
    2. 为其编写单元测试。确定方法应该实现的功能
    3. 测试,无法通过。^_^,因为没有方法实现嘛。但这一步必不可少,以免单元测试的代码有误,无论是否正确实现方法功能测试都可以通过
    4. 实现开发代码。比如在方法中完成方法体。
    5. 再次测试。如果通过,Over;否则,查找原因,修复,直到通过。

    以上述Student.Grow()的需求为例:

    首先,在Student中定义该方法但不要有真正的实现,所以可以是这样的:

        public class Student
        {
            public int Age { get; set; }
            public void Grow()
            {
                //没有方法实现
            }
        }
    

    然后,为该方法编写一个单元测试:

            [Test]
            public void Grow()
            {
                //测试准备:得到一个学生对象,其年龄为18岁
                Student student = new Student();
                student.Age = 18;
    
                //调用Grow()方法
                student.Grow();
    
                //检查是否实现了预期的结果
                //该学生的年龄变成了19(=18+1)
                Assert.AreEqual(19, student.Age);
            }
    

    注意我们是在一个新项目中测试另外一个项目,一个项目使用另外一个项目的代码,必须要添加引用。

    演示:接下来,不要忘了要跑一遍这个测试,当然这个测试是无法通过的。

    再然后,才去完成方法Grow():

            public void Grow()
            {
                Age++;
            }
    

    再跑一遍测试,通过!收工,^_^

    为什么要这么做呢?为了避免你的开发代码影响了你的测试思路

    同学们注意调试和测试的区别:调试是为了实现功能修复bug,而测试是为了找到bug!换言之,测试就是要get到你开发没有get到的点上去。如果你先写了开发代码,脑子里已经有了实现的细节,那就很容易出现:写的测试代码,无非就是把开发代码再“翻译”一遍,这样的测试几乎没有意义。

    你说,我其实也没看出来你上面这个单元测试有啥意义,^_^

    Wonderful!这说明你是带着脑子在听课的。

    为了表现出单元测试的意义,我们来完成这样一个功能:

    双向链表

    大家看我们一起帮的文章单页,每一篇底部都有一个“上一篇”和“下一篇”

    对应到文章对象,是不是它里面就应该包含两个属性:Previous(上一篇)和Next(下一篇)。我们再把它进一步的抽象,不局限于文章,就可以得到这样一个数据结构对象:

        public class DoubleLinked
        {
            public DoubleLinked Previous { get; set; }
            public DoubleLinked Next { get; set; }
            public int Value { get; set; }
        }
    

    因为每一个对象都有,就可以串成一串,这就是所谓的双向链表。用图表示:

    双向链表是有头(Head)和尾(Tail)的,头前面没有节点,尾后面没有节点。用代码表示就是:

            public bool IsHead
            {
                get
                {
                    return Previous == null;
                }
            }
    
            public bool IsTail
            {
                get
                {
                    return Next == null;
                }
            }
    

    注意:DoubleLinked既可以看成是双向链表中的一个节点,也可以看成是双向链表本身——因为从这个节点出发,向前(Previous)向后(Next)就能够获得全部的节点;即使是双向链表,也不会存储所有节点,而是存储一个头或/和尾即可。这里为了简便,就直接使用DoubleLinked进行各种操作了。

     

    现在我们来实现双向链表中最

    基本的操作

    ,插入一个节点,如下图所示,把节点5查入2和3之间。

    方法很简单:

    1. 把2的下一个指向5
    2. 把5的下一个指向3
    3. 把3的上一个指向5
    4. 把5的上一个指向2

    但代码怎么实现?你先想一想,^_^

    1. 首先,转变思路,把“查入2和3之间”转变成“插入2之后(InsertAfter(2))”,这样是不是就简单多了?
    2. 然后,你得想想,还需要指明“把谁”插入节点2之后?是不是要在InsertAfter()中再添加一个参数?
    3. 最后,InsertAfter()这个方法放哪里?静态的还是实例的?

    通过前面的学习和作业练习,我们知道了两个原则:

    • 能够实例就不要静态
    • 尽可能的减少方法参数个数

    所以,我们应该定义这样的一个实例方法:

            /// <summary>
            /// 在node之后插入当前节点
            /// </summary>
            /// <param name="node">在哪一个节点之后插入</param>
            public void InsertAfter(DoubleLinked node)
            {
            }
    

    OK,方法有了,你马上就撸柚子准备实现了……停停停!我们要先写单元测试。事情没有你想象的那么简单,你要不信这个邪呢,我们后面还有作业,你可以直接试一试。

    趁我们现在头脑还清醒的时候,先想想测试的事。

    首先我们要添加一个InsertAfterTest()方法,注意不要忘记在这个方法上添加[Test]特性,否则它不会被当做测试方法被NUnit调用运行:

            [Test]   //不要忘记[Test]特性
            public void InsertAfterTest()  //测试方法也不需要任何返回值
            {
            }
    

    为了测试,我们是不是首先要构建一个链表?然后才能往里面插入啊,怎么构建呢?只有手工,在InsertAfterTest()中添加:

                //在单元测试中,命名可以带123等后缀区分
                DoubleLinked node1 = new DoubleLinked();
                DoubleLinked node2 = new DoubleLinked();
                DoubleLinked node3 = new DoubleLinked();
                DoubleLinked node4 = new DoubleLinked();
    
                node1.Next = node2;
                node2.Next = node3;
                node3.Next = node4;
    
                node4.Previous = node3;
                node3.Previous = node2;
                node2.Previous = node1;
    

    然后,再新建一个inserted节点,将其插入节点2之后:

                DoubleLinked inserted = new DoubleLinked();
                inserted.InsertAfter(node2);
    

    OK,完成插入过后,应该是怎么样的一个情形?我们用代码表示:

                Assert.AreEqual(inserted, node2.Next);
                Assert.AreEqual(inserted, node3.Previous);
                Assert.AreEqual(node2, inserted.Previous);
                Assert.AreEqual(node3, inserted.Next);
    

    跑一跑测试,当然是跑不过的,因为InsertAfterTest()根本没实现嘛。

    好了,让我们去实现InsertAfterTest()方法吧……停停停!别慌,测试是为了找到bug,什么情况容易出bug,

     

    极端情况

    下就容易出bug啊!什么是极端情况,想一想,有了:如果是在链表的尾部插入呢?是不是也应该测一测?

    这时候我们有两种选择:

    1. 继续在InsertAfterTest()中添加Assert行
    2. 新开一个方法InsertAfterTailTest()

    我们就用第2种吧,看上去更规范更清晰一些。

    这时候就会有一个问题,是不是要在InsertAfterTailTest()中把构建链表的代码再写一遍?你说不用,我可以复制粘贴!你真是个机灵鬼,记住:程序员憎恨ctrl+c加ctrl+v

    我们的单元测试类还是一个类,这个类里面一样可以有各种类成员,比如字段方法属性等等。既然这些链表节点可以反复使用,我们为什么不把他们定义为字段呢?再回想一下我们的[Setup]特性,它是会在每一个测试方法被调用前运行一次的。我们可以在这里面完成节点的链接:

            //在单元测试中,命名可以带123等后缀区分
            DoubleLinked node1, node2, node3, node4;
    
            [SetUp]
            public void Setup()
            {
                node1 = new DoubleLinked();
                node2 = new DoubleLinked();
                node3 = new DoubleLinked();
                node4 = new DoubleLinked();
    
                node1.Next = node2;
                node2.Next = node3;
                node3.Next = node4;
    
                node4.Previous = node3;
                node3.Previous = node2;
                node2.Previous = node1;
            }
    

    于是,InsertAfterTailTest()里面的代码就非常简单了:

            [Test]
            public void InsertAfterTailTest()
            {
                DoubleLinked inserted = new DoubleLinked();
                inserted.InsertAfter(node4);
    
                Assert.AreEqual(inserted, node4.Next);
                Assert.AreEqual(node4, inserted.Previous);
                Assert.AreEqual(null, inserted.Next);
            }
    

    (InsertAfterTest()方法一样按此精简,此处略过)

    那还有没有其他“极端情况”?有,但飞哥不告诉你,接下来做作业的时候自己去想!^_^

    终于,我们可以实现InsertAfter()并运行单元测试了……

    演示:稍有不慎就无法通过测试,按下葫芦浮起瓢:

    这里有一个小技巧:先专注于通过最常规的InsertAfterTest(),然后再想办法同时通过InsertAfterTest()和InsertAfterTailTest()。

    好了,一路改,千辛万苦通过了这个单元测试,如下所示:

            public void InsertAfter(DoubleLinked node)
            {
                if (node.Next == null)
                {
                    node.Next = this;
                    this.Previous = node;
                }
                else
                {
                    this.Next = node.Next;
                    this.Previous = node;
                    node.Next = this;
                    this.Next.Previous = this;
                }
            }
    

     

    然后,你看这if...else里面好像有一些重复代码,比如:

    node.Next = this;
    this.Previous = node;
    

    这不是重复代码么?可不可以提出来?进行

    重构

    其实飞哥之前给同学们进行作业点评。如果你的代码没有错误,但我还是给你改了,这就是在做重构

    在不改变代码运行结果的前提下,优化代码质量(安全、性能和可读性)

    不知道大家有没有听说过一句话:

    好代码都是改出来的。

    很少有人一次性的写出非常完美的代码——尤其是代码会随着业务逻辑不断变化的时候,你根本就不可能一次性的完成代码,一定是不断的修修补补。但是,实际开发中,你会发现“修修补补”就会把代码慢慢地变成了“屎山”。最有越改越烂,哪有什么“千锤百炼”?!

    可以想象的一个场景:你满怀激情地正准备要重构,被你项目经理一把扑倒在地,“小子,不要命啦!?”

    为什么?

    你试试重构一下我们刚才的代码,按照我们想的:

            public void InsertAfter(DoubleLinked node)
            {
                node.Next = this;
                this.Previous = node;
    
                if (node.Next != null)
                {
                    this.Next = node.Next;
                    this.Next.Previous = this;
                }
            }
    

    看起来代码是整洁多了!然而,就在你沾沾自喜的时候,跑一下单元测试试试?

    这就是为什么不能重构的原因:

    没有单元测试做保证,你的重构风险太大

    其实添加新的feature(功能),修复旧的bug也一样,很容易对其他代码产生干扰,引入新的bug。而且这些bug可能很隐蔽,不一定能够被及时发现——除非你有单元测试。有了单元测试,每次代码改动,把所有的(注意,是所有的!)单元测试跑一遍,都跑过了,就证明改动没有影响现有代码。

    所谓TDD,其实就是要求所有的开发代码都有对应的单元测试(因为你要先写单元测试再写开发代码嘛),用单元测试来保证代码的:

    • 正确性。理论上,TDD的代码bug率非常低——那得你单元测试和开发代码都有疏漏,且双方的疏漏“相兼容”才行。否则,开发代码的bug会被单元测试暴露出来;单元测试的bug也会被开发代码暴露出来。
    • 可维护性。这其实才是TDD最重要的价值。以后同学们会越来越多的体会到代码维护工作的难度和重要性。业界有一句非常著名的论断:
    一个项目,开发所需的时间要占20%,而维护的时间要占80%

    同学们进入工作岗位,更大概率也是进行代码的维护工作(添加新feature,修复老bug等),而不是从头开发。如果没有单元测试覆盖,很多时候维护工作就是“头疼医头脚疼医脚”,修复了旧的bug,带来了新的bug。形象的比喻就是:

    • 这里有个坑,我在旁边挖点土填上,于是旁边又有了一个坑;
    • 好丑的一坨屎,怎么办?再上面再拉一坨屎盖住它!于是那些历史遗留代码都被称之为屎山。

    目前来说,TDD是一个理论上能够大幅度降低代码维护成本的方法。但注意飞哥用的“理论上”三个字,啥意思呢?实际上,开发过程真正做到TDD的不多,甚至可以说非常少。而TDD也从诞生之初的赞叹不止,变得越来越有争议。

    究其根本原因,飞哥认为,无他:

     

    成本和收益

    考量而已。最基本的事实,使用TDD开发,代码量至少翻番,值得么?确实,TDD可以降低后期的维护成本;但是,降低多少呢?和现在的投入相比,收益如何呢?更重要更重要的一个问题:能这个项目有后期维护么?99%的互联网项目,根本就活不到后期维护好吧?

    另外,单元测试不是那么好写的。尤其是涉及到数据库,涉及到外部调用接口,项目变得越来越复杂耦合度越来越高的时候……,这些需要同学们以后逐渐体会。同学们目前只需要记住两点:

    1. 能够单元测试的代码,一定是(高质量的)非常容易解耦的代码。
    2. 能写出高质量代码的程序员,工资一定是不低的

    所以,归根结底,还是成本问题。

    就飞哥个人而言,更愿意取一个折中:

    仅为“核心”代码使用TDD,引入单元测试。

    什么是核心代码呢?大致来说,复杂的、被大量使用、被反复修改的……,都可以算。但最终还是要靠开发人员根据实际情况具体掌握了。

     

    作业

    1. 为之前作业添加单元测试,包括但不限于:
      1. 数组中找到最大值
      2. 找到100以内的所有质数
      3. 猜数字游戏
      4. 二分查找
      5. 栈的压入弹出
    2. 继续完成双向链表的测试和开发,实现:
      1. InerstBefore():在某个节点前插入
      2. Delete():删除某个节点
      3. Swap():交互某两个节点
      4. FindBy():根据节点值查找到某个节点

    每日单词

    -------------------------------

    源栈第二期,飞哥开始编写更优质的课程讲义了。

    太基础的就没有发到园子里,但这一篇TDD相关的,有那么一点点意思,先发到园子里试试水,如果觉得可以的话,别忘记点个赞。以后有好的,我也都发到园子里来,^_^

    出处:https://www.cnblogs.com/freeflying/p/11983193.html

    =======================================================================================

    关于拒绝测试驱动开发(NoTDD)的讨论

    今天在reddit看到微软某大牛的博客(https://blogs.msdn.microsoft.com/ericgu/2017/06/22/notdd/),说到拒绝TDD(测试驱动开发,下文统一使用TDD)的事情。我很有感触。感兴趣的可以看看原文,我大概总结一下原文的意思(TL;DR):1、大多数TTD做的好的,都是设计和重构牛逼的;2、如果TDD的第三步(重构)去掉,剩下的只有一堆耦合很高的测试,写测试耗时很长,测试很难写。所以新手在学习编程时,学会设计和重构才是最重要的,而设计和重构是通过经验获得的,没有很死的规则。

    下面有个很有意思的评论,说现在只要有人反对TDD的思想,别人就会鄙视他/她。那些TDD的推崇者,说TDD做不好的,是“没有按照正确的方式”来做。于是团队里某个TDD拥护者想给其他人展示“正确的方式”:写了一个小程序,包含300个测试;程序到了QA,几天内发现90个bug;都是典型的UI bug;修复前两个bug花了三天,因为很多测试要重写……

    上面的例子有些夸张,不知道是否属实。我对TDD有大概的认识,不过向来不感冒。大概是因为我对于编程的理解,和TDD有本质的不同。TDD要求测试优先,我不知道有没有人在实践中,会先把测试写出来,再去写代码。这个是我不能理解的。如果要写测试,必然是要测试的东西都设计好了,接口成形了。实际上,我们往往在实现的过程中,才可以逐渐把接口抽象出来。也就是说,写程序的过程,是一个重构的过程,重要的是写出一些代码来,才有的重构,才有的测试。

    程序的设计是需要重构的,一开始没有人能把所有细节想清楚,往往一开始是不管设计的,只要写出来一个能够用的程序,可能设计很糟糕,但是至少有个重构的对象,剩下的就是在不改变功能的情况下,来对程序进行各种变换修改了。作家写作也是一样的,先不管他,把能想到的都写出来,然后再调整、修改、润色,这和写程序一模一样。

    不知道大家看到大触绘画没有,或者雕刻家雕刻,这些都有个共同的特点,就是一开始只是画个大概和轮廓,然后再一遍一遍地雕琢细节。我认为写程序本质上和画画、雕刻、写作等艺术创作的过程是一样,你有个灵感,写一些东西,重构一下,再重构一下,直到你认为成形了。唯一不一样的,是程序的有些部分需要实际运行才能确认是否正确,我想这才是测试的价值:你对于某一部分没有信心,所以写个测试来确保这块能够正常运行,通常这部分都是和外部系统交互的。

    把测试提到最重要的部分,用测试来保证程序的正确性,是本末倒置的表现,也是一种教条主义。就像所谓各种敏捷的方法论一样,虽然有一定的价值,然而按照全部规则来做的,无疑会出现各种问题。团队都是不一样的,敏捷也要理解精髓,自定义实践,而不是拿别人总结的规则来生搬硬套。

    出处:https://www.cnblogs.com/leading/p/about-notdd.html

    =======================================================================================

    也谈TDD,以及三层架构、设计模式、ORM……,天下没有免费的午餐

    想在园子里写点东西已经很久了,但一直没有落笔,忙着做 一起帮 的开发直播,还有些软文做推广,还要做奶爸带孩子,还要……好吧,我承认,真正的原因是:

    太特么的难写了

    但再难写也要写啊,要等到“能写好了再写”,怕是黄花菜都凉了——尤其是技术类文章,时效性非常强的。

    刚好坛子里这篇博客:关于拒绝测试驱动开发(NoTDD),看评论争议不小,而这个问题也是我最想写的,所以,蹭个热点,呵呵。

    其实我很好奇,博客下面热烈讨论的童鞋,有多少人是真正的在项目中坚持过TDD的。

    我公司里的项目,从来没有哪一个项目是要求TDD、能够TDD的;我自己的项目,坚持过TDD一段时间,而且应该是非常久的一段时间,尤其是Entity部分,但现在我基本上都已经放弃了。

    为什么呢?

    可以洋洋洒洒千言万语,也可以简简单单三个字:不划算

    其实不仅仅是TDD,还包括三层架构、设计模式、ORM等等这些东西,存在大量的争论,莫衷一是:说它好的把它捧到了天上去,说它不行的批得它体无完肤,双方都有大牛为其站台,都可以一二三四五的列出长长的清单,而且每一条都很有道理……

    当讨论变成了一种辩论,当辩论变成了一种骂战,最后拼的就是谁的态度更坚决,谁的言辞更犀利,谁的声音更大……所以双方的观点更加的偏激、对立,而这其实无助于我们客观冷静的来分析问题。

    说理太枯燥了点,还是听飞哥讲故事吧,呵呵。

    最早,我刚接触“设计模式”。什么玩意儿啊?!整本书,就一个感觉,“脱了裤子放屁”。明明一个对象,new一下不就OK了么?什么Factory啊Builder啊,搞毛线呢?所以一直是云里雾里的,包括那些开闭原则、依赖倒置,都似懂非懂的,没帮上我什么忙。

    直到有一天,也不知是在哪里,我看到了三个字“上下文”,或者说一句话,大意是:只有理解了上下文,理解了设计模式想要解决的“问题”,你才能真正的理解设计模式。不知道是不是那时候积累也差不多了,茅塞顿开恍然大悟!

    我在架构之路(一):目标里说过:设计模式是药,看评论其实很多同学没有理解,对照这句话看能不能明白过来:理解了设计模式想要解决的“问题”……?要解决的问题就是“病”,没病就不要乱吃药;同理,没有“问题”,你也不要乱用设计模式。

    一通百通。

    所以从最基础的面向对象、到三层架构、ORM、以及敏捷开发、TDD……,所有这些概念方法,本质上都是要解决问题的,而且基本上也是能够解决问题的。

    而你,认为它“没用”,其实最大的原因是:你还没碰到这方面的问题

    在这里,大家一定要区分两个概念:“它解决不了问题”和“它(对我)没用”。还是用药做比喻,“这药治不了病”,和“这药(对我)没用”是两个概念。而且尤其要注意的是这两个字:对我

    换到项目中,就是这种架构这种开发模式,适不适合这个项目,能不能解决这个项目开发中遇到的问题。

    其实之前我也看到过类似的提法,比如:xxx适合“大”项目。但用“大”和“小”来区分项目,毛糙了一些,很多时候,并不见得正确。最正确的做法是:你了解项目的特点,同时也了解各种模式的优劣,从而能够正确的匹配和选择。当然,这是一个非常庞大的话题,这里没办法展开了。

    好,上面我们提到了“优劣”,所谓优势和劣势,但其实,这个提法并不准确。优势,大家都可以承认,解决了问题嘛;但劣势……什么叫做劣势?不服……

    我更愿意用另一个词:成本。

    “天下没有免费的午餐”。

    这是一个经济学上的谚语。一提到这话,我就想起我大学的时候坐在教室里听老师讲《西方经济学》……往事历历在目,谁曾想,我会是今天这个样子?

    再说点题外话吧。

    【野生程序员】:优先招聘是意气之作,但并非完全意气用事,在我该不该转行?(一)野生程序员的优势一文里,我就较为详细的阐述了野生程序员的优势。简单的说:做架构,做项目管理,需要一个更宏大的视野,而不仅仅是二进制和计算机原理。

    这里,我们还是回过头来看,什么叫做“天下没有免费的午餐”?不要理解为“做人不要贪心以免上当”之类的哟!你可以理解为:做任何事情都需要成本。但我更喜欢另一种说法:凡是选择,必有代价

    具体到项目中,不管(注意是不管,无论,随便……)你选择是不是遵循TDD的规范要求,只要你选择了,就必然有代价:

    • 不使用TDD,就会在代码的重构、维护、健壮性等方面付出代价;
    • 使用TDD,就会在测试代码的开发和维护上付出额外的代价。

    无论你怎么选,一定是要付出“代价”的。换言之,代码的“低耦合”“可测试”“便于重构”……不可能从天上掉下来,一定是有成本的!

    这本来是一个最简单不过的道理。

    然而,当我们迫切的想达到一种目标——尤其是这种目标是美妙的、神圣的、寄托了我们某种强烈情感的时候,我们常常会忘记达成这个目标的成本。

    就个人而言,就是通宵达旦废寝忘食乐此不疲,这是你自个儿的事;但对于团队,对于项目呢?“不计一切代价”就是一种蛮干就是瞎搞,后果往往是灾难性……

    另一个很有意思的现象:我们的舆论,我们的文化,是鼓励“不惜一切代价”是鼓励“克服重重困难”的,这会让我们有一种莫名的冲动、一种热血沸腾的快感。理智和感性天然就是不兼容的?

    那么,我是反对TDD的?

    如果你心里还有这样的想法,说明你还是没弄明白我在说什么。

    无所谓支持和反对,没有这样简单化的答案。

    事实上,你需要的,是做一个成本和收益的分析:针对特定的、具体的项目!

    没有一个放之四海而皆准的准则。

    不同的项目,有不同的要求,应该因地制宜的采取相应的策略。

    这样谈下去还是会很空,我以 一起帮 为例。

    我为什么要放弃TDD?因为我对这个项目没有太大的信心,我目前最需要的,是尽快的把项目的原型拿出来,放到市场上进行检验:大家喜不喜欢,有没有前景,收集正面的反面的意见反馈……如果大致符合预期,我就继续做下去;否则,就要快速的进行调整。而我现在的人手又非常有限,好吧,其实就我一个人,所有的代码都得我一个人写;好在网站出bug问题不是很大,所有的用户都是种子用户,他们可以直接的给我反馈而不会因为一两个bug离我而去……

    所以综合上面种种考虑,我并不需要TDD,至少暂时不需要。也就是说,代码质量差一点就差一点,可以忍受。如果项目击中了用户的痛点,我可以以后花更大的代价来“补”;如果项目针对的是一个“伪需求”,我就应该尽快止损。

    你看,并不是TDD不好,并不是TDD没用,而是我现在“用不着”——这才是三观最“正”的,最无懈可击的理由。·

    顺便说一下,我现在采取的策略,我把它称之为“懒人策略”:一开始不写unit test,但一旦出现bug,fix bug之前,首先写unit test,然后在fix。(惭愧啊,仔细想想,这一点我都没完全做到,(⊙﹏⊙)b)

    其实我觉得呀,当然仅仅是“觉得”了,大多数的“大牛”们,其实是明白这一点的——虽然他们从没有像我这样系统明确的表述出来。

    我这样推断的原因是:现实中确实没有太多TDD实践的项目。

    实践TDD的机会其实是非常渺茫的,就我目前能想到的:

    1. 开发团队,尤其是架构师必须有相当的水平。我在架构之路(三) 单元测试就讲过:单元测试不是那么好写的!凡是可(易于)测试的代码,一定是“低耦合”的,模块之间是具有相当大的“独立性”的,不然相互牵连,将非常难以测试。而随着业务逻辑的耦合度(复杂度)越来越高,解耦的难度也就越来越高。反正据我的观察,一般的开发团队根本hold不住。有时候想想,非常之诡异:耦合度不高的项目,其实又没有多大的必要做TDD?
    2. 项目负责人对项目能够长期存活具有强大的信心。TDD的实践,是前期投资,后期收获。相当长一段时间,你都会觉得写单元测试非常无聊,只有到了后期,业务逻辑越来越复杂,到处都是千丝万缕的联系,牵一发而动全身,经常一改动单元测试就跑不过的时候,你才会觉得“咦?这玩意还真的有用呢!”但是,注意这个但是,项目负责人有没有足够的信心:这个项目能撑到那个时候?!市场朝秦暮楚变化无常,几乎所有人都是走一步看一步,摸着石头过河,哪里能顾得那么长远?
    3. 项目从一开始就不赶工期,允许使用大量(至少是双倍)的时间来写单元测试。就算是我有信心这个项目没问题,但时间允许不允许?商场上争的就是一个先手,快鱼吃慢鱼,要快,要抢先占领阵地。这就和强行军一样,确实有很多问题,不如步步为营稳妥,没有重武器,会有掉队减员,部队非常虚弱……但只要先到达阵地,其他一切都在所不惜。

    所以,我非常好奇,究竟有多少童鞋真正参与过一个严格按TDD模式实施项目?

    那么,TDD是不是就不值得学习了呢?

    当然不是的!

    +++++++++++++++++++++

    真的顶不住了!

    12点了,超级 =_=

    展开写还有很长很长,强写脑力也跟不上了。先这样吧,有时间我们下次再聊,晚安,各位。

    呵呵,偶然中发现的,小小的一个成就,纪念一下。

    出处:https://www.cnblogs.com/freeflying/p/7080244.html

    =======================================================================================

    个人的思考

    如果大家都当过项目经理,都学过项目管理,那么你一定听说过铁三角,你知道铁三角是怎么来的吗?

    前面都说了这么多了,无非就是你的付出和回报,就是投入时间成本,你最后收获的是一份质量,我们需要在付出和回报之间找到一个适合自己的 “完美” 分割线,一个平衡点而已。  

    那么铁三角的图示,看看下图你就明白了。

    中间的蓝色区域,是不是就是一个三角形了呢?我们取出来就是这个样子的。

      

     我想在你的心里已经有答案了,项目不同,人员不同,团队不同,都有差异,适合自己的,才是最好的。

  • 相关阅读:
    VMware Tools的安装
    XmlSerializer
    string[][]和string[,] 以及 int[][]和int[,]
    Get Length 使用dynamic关键字
    Cocos2d-x 3.0final 终结者系列教程01-无论是从cocos2d-x2.x升级到版本cocos2d-x3.x
    Hosting WCF Service
    A*算法进入
    OpenCV面、人眼检测
    JAVA学习篇--JAVA两种编程模式控制
    采用Java语言如何实现高速文件复制?
  • 原文地址:https://www.cnblogs.com/mq0036/p/16292736.html
Copyright © 2020-2023  润新知