张大发是一个有进取心的程序员。除了日常工作,他还学习单元测试和重构等编程实践。那天晚上,他在微信群里看到了关于一个叫TDD的东西的激烈争论,不由得产生了兴趣,于是上网搜索了一下。原来TDD就是测试驱动开发(TestDrivenDevelopment),强调测试先行,小步运行,用测试用例来驱动程序的界面和代码。1TDD的步骤看似很简单:1.写一个失败的测试用例2.写一点代码让这个测试通过3.重构代码(如果需要的话),转到第一步,张大发想,这三个步骤是不是它只是“把大象锁在冰箱里”?太抽象了!一点都不实用!他搜索了一些文章,发现这些文章讲的都是一些极其简单的例子,比如加减法计算器,货币换算等等。例如,在这个计算器示例中,第一步是编写一个简单的测试用例来测试将两个数字相加的行为。publicclassCalculatorTest{@TestpublicvoidtestAdd(){Calculatorcalculator=newCalculator();intresult=calculator.add(10,20);Assert.assertEquals(30,result);}}第二步在Calculator中实现add方法完成两个数字阶段添加逻辑以使测试通过。publicclassCalculator{publicintadd(inta,intb){returna+b;}}的逻辑极其简单,无需重构。直接写一个测试用例,测试两个数相减的行为。这种情况一直持续到所有功能完成为止。张胖子撇撇嘴:这就是TDD?太没有技术含量了。我明天会在项目中尝试它!,根据订单金额扣除优惠券,并按照规则为用户增加相应积分。张大胖看了看这个计算规则,很简单,估计一个函数就可以搞定。好吧,下面就带大家试一试TDD这把刀,看看TDD是好是坏。第一步是先写一个失败的测试!张胖子很清楚这个系统用的是Spring,典型的Controller->Service->DAO。Controller里面完全没有逻辑,调用Service就可以了。那么我们就直接在Service层写单元测试吧。张大胖很快定位到这个新需求相关的类,也就是OrderService的submit方法。TDD本来是为了驱动接口的,现在好像不需要了,已经存在了。张大胖看了一下接口的输入输出:publicclassOrderService{publicStringsubmit(StringrequestBody){...}}这个方法的输入参数居然是一个XML字符串!它包含诸如couponID、addressID之类的内容。.....xxxxxxxxxxxx......返回值也是一个XML字符串,指示成功或失败(以及相应的失败消息)。xxxxxxxx这年头还在用XML作为参数,只能说这是老应用了!3按照TDD的节奏,张大发写了Download第一个测试用例,让它失败。publicvoidOrderServiceTest{publicvoidtestBonusPoints(){StringrequestBody=......;//执行提交方法Stringresult=orderService.submit(requestBody);??验证积分,但是怎么验证??}}等一下,这个测试的入参是easyBuild,但是submit方法的返回值根本不会包含积分信息!那我算出来的点怎么可能是对的呢?是否可以让提交方法返回点数据?然后修改原来的通用接口协议,太烂了没错!第二个问题很快就出现了。积分计算的逻辑很简单,但是需要两条信息,订单总金额和优惠券。但是在测试用例中,这两条信息是从哪里来的呢?订单总金额需要在购物车中存入数据库,优惠券ID在提交方法的参数中,明细也在数据库中。积分的计算这么简单,难道我要先在数据库中创建购物车和优惠券,然后通过ShopCartService和CouponService从数据库中读取出来吗?这也太反常了吧?不行,单元测试一定要避开数据库,一定要用Mock方法。张大发知道一个Mock框架叫Mockito,非常好用,所以就用了。张大胖浏览了2000多行的OrderService.submit函数。没关系。张大胖发现这个功能依赖了另外七八个服务:UserService、ShopCartService、CouponService……这几个服务有的严重依赖数据库,有的严重依赖Http,有的依赖消息队列。也就是说,为了让submit方法能够顺利执行,必须对这七八个服务进行Mocked,让它们协调工作。例如,如果给定了userID,则可以返回正确的用户对象。给定couponID,可以返回正确的优惠券对象。Mockito可以实现这个功能,但是协调七八个Service相关的对象需要很多代码!测试用例中的代码会变得非常复杂和脆弱。张胖子傻眼了!我连测试用例都写不出来,那我为什么要做TDD?4张胖子叹了口气,放弃了写测试用例的想法,在OrderService.submit方法中找到了一个合适的地方,然后根据订单金额和优惠券信息,写了几十行代码,计算积分,并将它们保存在数据库中。然后他启动了程序,通过界面提交了几个订单,覆盖了各种情况,做了人工测试,然后查了数据库。他欣喜地发现,积分计算完全正确。花了不到一个小时。什么TDD,让它见鬼去吧!后记:其实真正的TDD并没有文中的三步那么简单。TDD的正确做法是根据需求编写粗粒度的功能测试。这些测试可以驱动程序的界面,然后编写细粒度的单元测试来驱动出详细的代码。今天的文章太长了,就不展开了。咱们再写一篇文章说说吧。理解了TDD的思想之后,再转变思路去实施TDD并不是特别困难。我这些年遇到的主要困难是遗留项目,代码很乱,可测试性差。要想写出清晰、好的测试,往往需要重构大量的代码,得不偿失。说是做TDD,其实很多时间都在重构代码,开发进度慢,看不到立竿见影的收益,所以放弃了。【本文为专栏作家“刘欣”原创稿件,转载请通过作者微信获取授权公众号coderising】点此查看该作者更多好文