博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

直奔主题,提高TDD开发效率

Posted on 2010-10-12 04:04  sinojelly  阅读(397)  评论(0编辑  收藏  举报

TDD真的违反人性吗?

 
TDD强调以极小的步子,一点一点的驱动出代码。
 
新手,往往对如何划分出小的步子比较疑惑,再加上看到的例子,都是最先给出极其简单的用例,于是,会在工作中总想着找到同样的极其简单的用例开始,但往往这是找不到的,结果在寻找最简单的用例上,也花费了不少时间。
 
而且,从寻找最简单用例入手,与原来的开发方式、思维方式冲突很大。它使得你哪怕已经胸有成竹,但不能写关键的用例或者代码。也许你非常想把现在想到的关键的东西实现为代码,但考虑到TDD你就矛盾了,这也许就是TDD“违反人性”的一面吧。
 
事实上,不是TDD在违反人性,而是人们误解了它,才感觉到它违反人性。TDD的核心在于“驱动”,驱动出设计,驱动出实现,至于采用步子的大小,可以根据个人能力,需求的难度灵活控制。记得《测试驱动开发》那本书上也是这样说的,大概是“如果你觉得一切尽在掌握之中,可以步子大一点;什么时候,发现难度大了不好控制,就步子小一点”。
 

步子大小的理解

 
步子的大小,不单纯指驱动出一行代码,驱动出一个变量类型的改变,我觉得它更大的意义在于,驱动出“简单”的类、模块设计。通过TDD的开发方法,驱动出的都是极小的类、模块,那么这也是小步。
 
对于我来讲,发现有难度不好控制,不好测试,往往都是还没有找到合适的设计,没有把被测对象清晰的识别出来,或者暂时看到的对象还职责太多,太复杂,使得不知道如何测试,如何写用例。这种时候,我倾向于更多的思考设计,需要动手才看得清楚我就会写spike代码,外行看起来我就像在裸奔,事实上不是的,因为现在写的代码不追求严谨,不写测试,只求探索出清晰的思路,理顺各个类的职责,而且最重要的一点是,现在所写的代码最后会删掉。
 
顺便说一句,有人认为TDD不需要预先设计,一切都在测试过程中驱动出来,我不这样认为,实施敏捷的人往往都喜欢spike,其实那就是思考整理设计的过程。
 

直奔主题,提高TDD开发效率

 
我认为,TDD过程中,直奔主题是非常重要的,它可以让你很快发现最核心的业务所在,解决最大的困难。
 
直奔主题,就是从写一个“具有基本功能的”、“具备一定业务价值的”、“比较简单”的用例开始。这种方式可以快速的驱动出接口,以及类、方法的部分有效实现,比一味的追求从极简单的用例开始,效率要高得多。追求简单用例,往往还在寻找哪个是最简单的用例上花费太多时间。
 
不追求最简单这样的细枝末节,也不追求极小的步伐,而是采取跟自己能力相适应的步伐大小来做开发,既不会让人觉得别扭,或者“违反人性”,又很快的直奔主题,解决了业务上的问题,同时保留了TDD的效果:驱动出接口、从使用者角度思考接口、留下较完善的用例(步伐大,用例遗漏的可能性有可能加大,需要注意业务场景尽可能都要覆盖)、用例即文档。
 
使用这种方法之后,就不会在TDD本身上花费太多时间,多数时间都是花在设计上,设计上的时间花费是不可避免的,也是值得的,随着经验的增长,设计效率会提高。
 

TDD驱动接口设计的实例

 
TDD在驱动接口设计方面的作用是毋庸置疑的,下面是一个实际的例子,一开始,我对被测类TestInfoParser的接口只有模糊的想法(当然职责是清楚的)。通过写一个简单的,基本业务场景的测试用例,很容易就看清楚什么样的接口用户容易使用了。
 
        [TestMethod]
        public void simple_test_file()
        {
            string fileContent
                = "FIXTURE(FixtureName)\r\n"
                + "{"
                + "    // @test(id=first-id)"
                + "    TEST(测试用例1)\r\n"
                + "    {\r\n"
                + "    }\r\n"
                + "    \r\n"
                + "    // @test(id=second-id, depends=first-id)"
                + "    TEST(测试用例2)\r\n"
                + "    {\r\n"
                + "    }\r\n"
                + "};";
            
            MemoryStream stream = new MemoryStream(System.Text.ASCIIEncoding.ASCII.GetBytes(multiLines));
            
            Assert.AreEqual(true, parser.parse(stream));

            Assert.AreEqual("FixtureName", parser.Fixtures[0].Name);
            
            Assert.AreEqual("测试用例1", parser.Fixtures[0].Tests[0].Name);            
            Assert.AreEqual("first-id", parser.Fixtures[0].Tests[0].id);

            Assert.AreEqual("测试用例2", parser.Fixtures[0].Tests[1].Name);            
            Assert.AreEqual("second-id", parser.Fixtures[0].Tests[1].id);
            Assert.AreEqual("first-id", parser.Fixtures[0].Tests[1].depends);    
        }

 

设计与TDD相辅相成


再列举一个设计与TDD的例子,来自上面例子同一个项目。我需要解析测试用例文件,得到其中定义的FIXTURE/TEST。很显然,用正则表达式就可以了,我打算用一个TestInfoParser类来完成解析功能。

当时我发现,我想验证我写的FIXTURE、TEST、Annotation语句的正则表达式解析是否正确,而实际上TestInfoParser太大,不好直接测试这些东西。

于是,我划分了FixtureParser、TestParser、AnnotationParser三个类,分别实现三种类型字符串的解析,而且每次解析都只解析一行代码,这样测试就非常容易实现了,当然被测类也简单了。
写这样简单的代码,损失脑细胞少多了,而且也不用麻烦的调试,犯错误的概率也大大降低,这也许就是分离关注点的好处吧,大事化小。

不仅如此,分为三个类之后,TestInfoParser与它们的配合也极其简单,依次调用每个Parser,有一个成功解析就可以开始下一行的解析了。
 

TDD小贴士

 
1、多用TODO注释记录待办事项。
TDD开发过程中,需要同时关注测试和代码,虽然测试也是需求的描述,也是设计实现验证,本质上是不冲突的,而是与开发过程相辅相成的,但是,毕竟需要多干一些活,可能使得某个时候想到的一个很好的想法,不能立即去实现(普通的开发方式也有这种情况),建议这种时候,在代码中用TODO注释写出待办事项,Visual Studio自带了TODO列表功能,可以显示所有的TODO项。TODO完成之后,可以把该注释删掉,如果觉得它有价值,也可以把TODO改为Note之后保留。
 
2、多用spike。
当你发现想不清楚时,就需要使用spike了,它也许让你感觉很亲切、很熟悉、很畅快,因为它某些方面有点像裸奔^_^
 
3、尽量结对开发。
敏捷开发中,追求高质量的代码,追求简洁可用,有弹性的软件,还要采用TDD的开发方式,难度相比普通的开发大了很多(当然开发出来的软件毋庸置疑会比普通开发方式好很多),这种情况下,结对开发是非常有必要的。讨论设计的时候,pair可以给你提示一些没注意到的地方,少走弯路;编码实现的时候,pair可以帮你发现BUG或者提出更优的方案,把问题消灭在萌芽状态。只要两个人都围绕开发工作沟通、交流、思考,肯定是很有帮助的,无论多牛的人,结对都还是很有必要的,因为是人都会犯错误,会犯错误就需要人协助和提醒,多一个人,很多时候会让你惊喜的发现能够找到更优的解决方案,比你们两个人独立做事都要好得多的解决方案。
实际操作中,可以采用一个人写用例,一个人写实现代码的方法。当然,也可以采用别的方式,最重要的就是心往一处想,劲往一处使。