[TDD]由SearchCriteriaBinder看Test Driven与Test First

很久没有维护blog,近两年新工作平平淡淡,生活迷迷糊糊,自己都不知道自己在做什么了,最近看了老赵 我的TDD实践:可测试性驱动开发(下)Shuhari用TDD方式实现老赵的SearchCriteriaBinder,一时手痒,也来凑凑热闹,赚赚人气.

我觉得老赵 之所谓觉得实施TDD很困惑,主要是没有把握好"Test Driven"和"Test First"之间的差别. 我们这里先不下结论.先演示我根据自己的理解,利用测试驱动开发SearchCriteriaBinder的过程.

1. 任务需求

构建一个Model Binder。ASP.NET MVC中Model Binder的职责是根据请求的数据来生成Action方法的参数(即构建一个对象)。那么这次,我们将为负责产品搜索的Action方法提供一个SearchCriteria参数作为查询条件

2.设计Class和第一个Test Unit

相关的数据class:

    public class SearchCriteria
   
{
       
public PriceRange Price;
       
public string Keywords { get; set; }
       
public Color Colors { get; set; }
    }


    [Flags]
   
public enum Color
   
{
        Red = 1,
        Black = 1 << 1,
        White = 1 << 2
    }


   
public class PriceRange
   
{
       
public float Min { get; set; }
       
public float Max { get; set; }
    }


   
public class SearchCriteriaBinder : IModelBinder
   
{
       
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
       
{
           
throw new NotImplementedException();
        }

    }


   
//由于没有安装MVC环境,所以这边用伪类代替,这个不影响我们的开发
    public interface IModelBinder
   
{
       
object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext);
    }


   
public class ControllerContext
   
{
    }


   
public class ModelBindingContext
   
{
       
public string RawValue
       
{
           
get;
           
set;
        }

    }

第一个TestUnit

    [TestMethod()]
       
public void BindModelOne()
       
{
            ControllerContext controllerContext = new ControllerContext();
            ModelBindingContext modelBindingContext = new ModelBindingContext() { RawValue = "keywords-hello%20world--price-100-200--color-black-red" };

            SearchCriteria criteria = (SearchCriteria)new SearchCriteriaBinder().BindModel(controllerContext, modelBindingContext);
            Assert.IsTrue(criteria.Keywords.Contains("hello world"));
            Assert.IsTrue(criteria.Price.Min == 100);
            Assert.IsTrue(criteria.Price.Max == 200);
            Assert.IsTrue(criteria.Colors == (Color.Black | Color.Red));
           
        }

 

3.用例写好了,运行下用例,看它是否可以通过,不能通过就修改实现代码,让其通过测试.

很显然,当前的代码是无法通过测试,所以我们修改下SearchCriteriaBinder 的代码,让它可以通过这个测试.

    public class SearchCriteriaBinder : IModelBinder
   
{
       
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
       
{
           
return new SearchCriteria() { Keywords = "hello world", Price = new PriceRange() { Max = 200, Min = 100 }, Colors = Color.Black | Color.Red };
        }

    }

4. 运行测试用例,通过了.

5. 根据需求获取新的用例.

如果你再也提炼不出用例,则证明任务已经完成.目前这个需求明显还没有完成,现在只针对一个固定条件进行测试/实现,而看需求,这个条件是需要被泛化(generalization) 利用三角法(Triangulation)我们获得了一个新的测试用例.

        [TestMethod()]
       
public void BindModelTwo()
       
{
            ControllerContext controllerContext = new ControllerContext();
            ModelBindingContext modelBindingContext = new ModelBindingContext() { RawValue = "keywords-hello%20cnblogs--price-200-300--color-White" };

            SearchCriteria criteria = (SearchCriteria)new SearchCriteriaBinder().BindModel(controllerContext, modelBindingContext);
            Assert.IsTrue(criteria.Keywords.Contains("hello cnblogs"));
            Assert.IsTrue(criteria.Price.Min == 200);
            Assert.IsTrue(criteria.Price.Max == 300);
            Assert.IsTrue(criteria.Colors == Color.White);

        }

 

6.获得新用例之后,先运行测试,失败,必须重写实现让用例通过.

 public class SearchCriteriaBinder : IModelBinder
   
{
       
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
       
{
           
//throw new NotImplementedException();
           
//return new SearchCriteria() { Keywords = "hello world", Price = new PriceRange() { Max = 200, Min = 100 }, Colors = Color.Black | Color.Red };
            var rawValue = bindingContext.RawValue;

            var text = HttpUtility.UrlDecode(rawValue.ToString());

            var tokenGroups = text.Split(new string[] { "--" }, StringSplitOptions.None).ToList();

            var fieldTokens = tokenGroups.ToDictionary(g => g.ToLowerInvariant().Split(new string[] { "-" }, StringSplitOptions.None)[0], g => g.ToLowerInvariant().Split(new string[] { "-" }, StringSplitOptions.None).Skip(1).ToList());


            var searchCriteria = new SearchCriteria();

            List<string> values;


           
if (fieldTokens.TryGetValue("keywords", out values))
           
{
                searchCriteria.Keywords = values[0];
            }


           
if (fieldTokens.TryGetValue("price", out values))
           
{
                searchCriteria.Price = new PriceRange
               
{
                    Min = float.Parse(values[0]),
                    Max = float.Parse(values[1])
                }
;
            }


           
if (fieldTokens.TryGetValue("color", out values))
           
{
               
foreach (var item in values)
               
{
                   
switch (item)
                   
{
                       
case "red":
                            searchCriteria.Colors = searchCriteria.Colors | Color.Red;
                           
break;
                       
case "black":
                            searchCriteria.Colors = searchCriteria.Colors | Color.Black;
                           
break;
                       
case "white":
                            searchCriteria.Colors = searchCriteria.Colors | Color.White;
                           
break;
                       
default:
                           
break;
                    }

                }


            }


           
return searchCriteria;

        }

    }

 

7. 重新运行两个测试用例,已经可以通过.

8. 重复5~7. 直到没有新的测试用例可以新增,任务完成.

    这边的测试用例,还有一个测试类别和完整性的问题.我们后面再讨论.

9.重构,完善代码.

   到了这一步,算是一个里程碑,接下来的步骤就会发生分歧.

吉日说:任务完成了,可以提交给客人.
老赵说:代码好丑陋,毫无设计可言.

   哈哈,调侃一下.TDD不仅仅是驱动开发,同时也有重构,完善代码的步骤(Test-Code-Test-Refactoring-Test-Code)

   重构就是在不改变可见行为的情况下,整理代码,去除坏味道,使其更加清晰可读.所以我们重构代码如下

  public class SearchCriteriaBinder : IModelBinder
   
{
       
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
       
{
           
//throw new NotImplementedException();
           
//return new SearchCriteria() { Keywords = "hello world", Price = new PriceRange() { Max = 200, Min = 100 }, Colors = Color.Black | Color.Red };
            var rawValue = bindingContext.RawValue;

            var text = HttpUtility.UrlDecode(rawValue.ToString());

            var tokenGroups = this.Tokenize(text);

            var searchCriteria = this.Build(tokenGroups);

           
return searchCriteria;

        }


   
       
private List<string> Tokenize(string text)
       
{
            var tokenGroups = text.Split(new string[] { "--" }, StringSplitOptions.None).ToList();
           
return tokenGroups;
        }


       
private SearchCriteria Build(List<string> tokenGroups)
       
{
            var fieldTokens = tokenGroups.ToDictionary(g => g.ToLowerInvariant().Split(new string[] { "-" }, StringSplitOptions.None)[0], g => g.ToLowerInvariant().Split(new string[] { "-" }, StringSplitOptions.None).Skip(1).ToList());


            var searchCriteria = new SearchCriteria();

            List<string> values;


           
if (fieldTokens.TryGetValue("keywords", out values))
           
{
                searchCriteria.Keywords = values[0];
            }


           
if (fieldTokens.TryGetValue("price", out values))
           
{
                searchCriteria.Price = new PriceRange
               
{
                    Min = float.Parse(values[0]),
                    Max = float.Parse(values[1])
                }
;
            }


           
if (fieldTokens.TryGetValue("color", out values))
           
{
               
foreach (var item in values)
               
{
                   
switch (item)
                   
{
                       
case "red":
                            searchCriteria.Colors = searchCriteria.Colors | Color.Red;
                           
break;
                       
case "black":
                            searchCriteria.Colors = searchCriteria.Colors | Color.Black;
                           
break;
                       
case "white":
                            searchCriteria.Colors = searchCriteria.Colors | Color.White;
                           
break;
                       
default:
                           
break;
                    }

                }


            }

           
return searchCriteria;
        }

    }


   10.重构之后运行所有测试用例,保证逻辑(可见的行为)没有被更改,这就是测试用例的另一个作用,保证原先的业务逻辑不被更改.

   11.私用对象是否应该被测试.

      原则上来说,私用对象是不需要测试.如果你想对他进行测试,则应该将它抽象出来,独立作为一个需求.这样子才不需要增加/修改对象本身的行为(public/protected)从而破坏本来的业务行为. 我们依然以SearchCriteriaBinder为例,进行下一步动作.

   12.抽象出新的对象出来,并重构原来的对象.

  internal class Tokenizer
   
{
       
public List<string> Tokenize(string text)
       
{
           
throw new NotImplementedException();
        }

    }

 

        public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
       
{
           
//throw new NotImplementedException();
           
//return new SearchCriteria() { Keywords = "hello world", Price = new PriceRange() { Max = 200, Min = 100 }, Colors = Color.Black | Color.Red };
            var rawValue = bindingContext.RawValue;

            var text = HttpUtility.UrlDecode(rawValue.ToString());

          
 var tokenGroups = this.Tokenizer.Tokenize(rawValue);

            var searchCriteria = this.Build(tokenGroups);

           
return searchCriteria;

        }



     
  private Tokenizer Tokenizer
       
{
           
get;
           
set;
        }


       
//private List<string> Tokenize(string text)
       
//{
       
//    var tokenGroups = text.Split(new string[] { "--" }, StringSplitOptions.None).ToList();
       
//    return tokenGroups;
       
//}

 

  13. 编写第一个Test Unit

      [TestMethod()]
       
public void TokenizeTestOne()
       
{
            Tokenizer target = new Tokenizer();
           
string text = "keywords-hello world--price-100-200--color-black-red";
            List<string> expected = new List<string> {
           
"keywords-hello world",
           
"price-100-200",
           
"color-black-red",
            }
;
            List<string> actual = target.Tokenize(text);
        
            Assert.AreEqual(expected[0], actual[0]);
            Assert.AreEqual(expected[1], actual[1]);
            Assert.AreEqual(expected[2], actual[2]);
           
        }

14.因为重构了代码,所以必须运行所有的测试用例.你会发现除了新加的这个测试失败了,以前的用例都失败了.因为Tokenizer没有被实例化,所以我们修改下SearchCriteriaBinder的constructor

        public SearchCriteriaBinder()
       
{
           
this.Tokenizer = new Tokenizer();
        }

  并实现Tokenizer的Tokenize方法

     public List<string> Tokenize(string text)
       
{
           
//throw new NotImplementedException();
            var tokenGroups = text.Split(new string[] { "--" }, StringSplitOptions.None).ToList();
           
return tokenGroups;
        }

  15. 重新运行所有的测试用例,通过

  16. 接下来泛化Tokenizer的参数,重新添加测试用例,重复之前的步骤.(至于build要重构也是类似的步骤).

 

以上的步骤,就是我理解中的TDD的开发顺序.你可以下载演示代码自己进行尝试

http://cid-af3411fff50fdeaa.skydrive.live.com/embedicon.aspx/Public/Demo/SearchCriteriaBinder.rar

接下来,稍微八下理论方面的看法.

    在老赵的文中,基本上是他已经先设计好了实现的思路/设计,然后再写出对应的测试用例,接下来进行编码.(好多"我想要单元测试",哈哈).以这样子的步骤进行TDD的开发,肯定是不会对原有的开发流程起到辅助作用的,相反,反而会增加很多负担,完全没办法发挥TDD的好处. 也就是老赵其实用的是"Test first",而不是靠Test来Dirven development的. 所以实践起来就觉得别扭.

    难道,TDD都要这样子一步步来嘛?我设计能力比较强,一下子就可以把整体的轮廓都设计出来,总不能让我倒退回去,像菜鸟一样一步一步来吧?

    没错,TDD是提倡小步前进,逐步测试,逐步实现. 但是其实这里面有步子迈得多大的问题. 步子迈得越大,就要求更好的综合设计能力/全局把握能力.

    举个简单的例子. 如果刚开始TDD,或者对刚学开发的同学来说,他肯定是按照上面的步骤一步步来更保险的,但是对于一般熟练的人员来说,他就可以直接泛化参数,一下子写好两个测试用例.然后再通过两个用例直接驱动出实现来.    所以这个步子的大小完全取决于你对自己能力的定位^_^,有人只能一步跨过一条水沟,但是有人就是可以一步跳到河对岸,人比人气死人哦,千万别去比

   另外一个问题,就是测试的完整性/覆盖率. 其实测试分类别的,一些是上层测试,一些是下层测试,如果你硬要把它们和在一起,会累死人的.比如上面的SearchCriteriaBinder的两个测试用例,从业务层面(上层),他们已经可以完整体现任务的需求,所以已经算是完整了. 如果你硬要把参数为空/参数结构非法也加进来,那应该让他们来驱动什么业务呢? 这些底层测试,是可以外包给类库,或者内部消化的. 你不能用他们来体现业务,驱动开发,当然也不是绝对的,可能某些特殊的情况正是业务需求也不一定.

   还有另外一个典型的说法,就是阿不在帖子中回复说的

阿不:
从老赵的这篇文章能够体会到,我自己平常很难在项目中实践单元测试开发的一个重要的症结在于设计和抽象能力的不足。

 

这样子的结果就是因为步子太大了,所以对全局观的把握能力要求太高了,导致自己觉得力不从心,好像能力不足的样子.也可是说,这样子的开发不是用测试用例驱动开发而得到的,而是需求经过脑袋(经验)进行整体设计之后,再往TDD上靠,自然就力不从心了,那么大的系统,脑袋的运算量毕竟有限啊.

 

总得来说,我认为要实践TDD,必须把握好二件事情:

1. 需求决定用例

2. 用例驱动开发.

特别是用例驱动开发,它就是我们通常说的"先有用例,再有实现",实现是用例驱动出来的,不是你脑袋先想出来的.脑袋想的应该是行为,比如SearchCriteriaBinderBindModel方法.为了养成这种习惯,必须牢记一句话,从javaeye的gigix那边听来的"do the simplest thing to make the test pass, not the most stupid thing", 这也就是为什么我们为了通过第一个测试,直接hardcode的原因

Test Driven 和Test First是两个完全不一样的概念,目的不一样,效果也不一样.

Tag标签: TDD
3
0
(请您对文章做出评价)
« 上一篇:[OpenSource] ScriptLoader V5:不一样的体验,客户端分布式缓存平台
» 下一篇:LINQ TO SQLite实践指南
posted @ 2009-10-20 22:43 浪子 阅读(1501) 评论(18)  编辑 收藏

  回复  引用  查看    
#1楼2009-10-20 22:59 | Jeffrey Zhao      
真不错,可惜我现在还有些事情,明天到了公司在细看,呵呵。多谢指点!
  回复  引用  查看    
#2楼[楼主]2009-10-20 23:59 | 浪子      
@Jeffrey Zhao
言重,期待更多人可以参与讨论,TDD在javaeye可以讨论很火的,不过园里好像冷清很多。

  回复  引用  查看    
#3楼2009-10-21 08:31 | Steven Chen      
学习学习,先留个名,来个连载吧
  回复  引用  查看    
#4楼2009-10-21 09:03 | 阿不      
果然牛,原来这方面的专家就在我身边呢。再读一遍
  回复  引用    
#5楼2009-10-21 09:09 | SonicChen[未注册用户]
呵呵,贡献一下人气
  回复  引用  查看    
#6楼2009-10-21 09:13 | 阿不      
你的这篇文章,又让我从走向另一个误区之路上拉了回来。也是,实践TDD如果都需要很好的设计,那么,门槛就太高了,而它应该是大众化的。
  回复  引用  查看    
#7楼2009-10-21 09:14 | 阿不      
@SonicChen
你都没有博客园的帐号???外星人

  回复  引用  查看    
#8楼2009-10-21 10:12 | Ivony...      
很明显,这里是在驱动开发了。

我一直很懒得写博客,原因很多。其中一个原因恐怕是我觉得其实在讨论中更能获取有价值的信息。但这也不是说我不乐意去分享什么东西,只是要分享一个对大家都有用,而又没人分享过的东西(我是懒人,有人分享的东西自然懒得再去啰嗦),比较难。

在这几天关于TDD的讨论中,我对TDD的了解越来越清晰,至少我个人感觉是这样。在LZ的实践中,我们可以明确的看到“驱动”的存在。


但LZ的实践也给我们提出了一个很好的问题,换言之也是我一直在思索的问题,TDD怎么运用到我们的项目中,什么是TDD的最佳实践场景。

TDD的小步前进的策略,对于很多开发经验丰富,抽象能力很强的人来说,可能更多的是一种桎梏,只会拖慢我们的开发进度,而对质量的提高却又显得不值一提。因为他们的抽象能力已经足以确保开发出高质量的软件。


那么,有没有大步前进的TDD呢?

或者说小步前进的策略,好处在哪里?


我想第一个问题我难以解答,但我可以谈谈第二个问题的看法。

简单的说,小步前进的策略可以确保我们在创造最有价值的东西。
作为开发人员,很多时候会故意混淆为这个项目创造价值,还是为自己创造价值。这在人类社会中也可以说是非常头疼的顽疾。

以前有一个很有意思的笑话,我摘录于下:
“联合利华引进了一条香皂包装生产线,结果发现这条生产线有个缺陷:常常会有盒子里没装入香皂。总不能把空盒子卖给顾客啊,他们只好请了一个学自动化的博士后设计一个方案来分拣空的香皂盒。博士后拉起了一个十几人的科研攻关小组,综合采用了机械、微电子、自动化、X射线探测等技术,花了几十万,成功解决了问题。每当生产线上有空香皂盒通过,两旁的探测器会检测到,并且驱动一只机械手把空皂盒推走。

中国南方有个乡镇企业也买了同样的生产线,老板发现这个问题后大为光火,找了个小工来说你他妈给我把这个搞定。小工果然想出了办法:他在生产线旁边放了台风扇猛吹,空皂盒自然会被吹走”

我不想去说谁的解决方案更好,但我个人感觉,那个博士后就是显然在为自己创造价值而不是其客户(老板),联合利华。

换言之联合利华只需要他解决问题a,他顺带着解决了问题集合A,成本增加了N倍,带给联合利华的效益却没有同步增加。如果你是联合利华你怎么想?


所以很明显,这都不能算是软件范畴的问题,而是整个社会的问题,人的自私本性。

未完待续。。。。

  回复  引用  查看    
#9楼[楼主]2009-10-21 11:17 | 浪子      
@Ivony...
其实我也没有在大型项目中应用。我想我们是不是又陷入了一个误区。

TDD的小步前进的策略,对于很多开发经验丰富,抽象能力很强的人来说,可能更多的是一种桎梏,只会拖慢我们的开发进度,而对质量的提高却又显得不值一提。因为他们的抽象能力已经足以确保开发出高质量的软件。”

你的这些结论是基于项目是一个人开发的情况下,而且这人抽象能力又很强,经验又丰富。 所以他的步子要跨得很大。 (跨得很大的意思,就是他不需要一步一个脚印用的用例来驱动设计,而是直接利用自己的经验设计出优秀的程序结构,然后再用用例来驱动代码实现,很接近老赵的状态。


我们说TDD有两层用处,
第一,需求抽取用例,用例驱动实现;
第二,消除重复,完善代码,保证重构的进行。

TDD并不排斥优秀的代码设计,关键要控制好步子,不要让实现脱离自己的控制。


“那么,有没有大步前进的TDD呢?”
我觉得应该有的,按老赵的例子来说,老赵应该对需求先进行抽象,从而得出一个设计好的程序结构(包含SearchCriteriaBinder,ITokenizer,IBuilder等),然后由用例驱动这些抽象的实现。

我觉得这边只是转换了一下上层建筑而已,第一种方法是由业务需求来决定用例,再驱动出实现,重构出设计。
第二种方法是,先分析业务需求,更改上层建筑为自己设计的程序结构(当然这个程序结构间接反映了业务需求),然后驱动开发,可能重构的步骤就省了(或者少了),完成所有的Test Case也就结束。

总得看来只是把重构的过程提前了而已,看起来似乎是没对老赵产生什么大的效益。所以对老赵来说,设计是直接来自于他的经验,而不是用例一步步驱动演变出来的。也就是程序的开始对老赵的作用只有别人的一半。

但是对于复杂的程序,能力再强也不可能一下子全部设计出最终结构来,还是需要重构迭代的,这个时候就由回归到TDD的范畴来了,当然他可能要重构的时候又一下子设计好了结构^_^,对他的作用只好又减半了。

不过TDD并不是只针对个人的,他也是对团队的,没错老赵是设计好了最终程序结构,但是实现可能是团队其它人做的,这个时候,用例就体现了需求说明的作用,同时保验证实现人员代码的最终结果。

我认为TDD应该是Requiredment -> Test -> Development,关键是对把需求和开发联系起来,所以也可以说这个是个消化需求的方法论,用小步前进有利于充分的理解需求,同时也可以帮助设计。当我们一下子设计不出来最终结果的时候,可以利用这种方法,帮助自己思考。

  回复  引用  查看    
#10楼[楼主]2009-10-21 11:34 | 浪子      
@Ivony
其实TDD关注的也是客户的需求,它不会未来可能出现的需求做用例,只为目前看到的需求做用例。如果你把这点和设计联系起来,进行适度的设计(而不是过度设计),它的好处应该还是可以体现出来。

总得来说,TDD对吉日的作用远大于对老赵的作用,这就是开发中个人对个人追求目标定位不一样产生不一样的效果。

其实TDD的目标只是“Clean Code That Work“,它更关注的是客人的价值实现(业务需求),而不是代码自身/程序员自身的价值。

这样子看来TDD是很适合商业软件开发的。

  回复  引用  查看    
#11楼2009-10-21 12:13 | Ivony...      
嗯,因为会议的原因。。。。。未完待续。。。。。

我觉得我们的观点是不谋而合的,不过在讨论我的观点之前,我想我还是把未完的给续上吧。


因为人的自私本性问题,开发人员的目标是实现自我价值而不是客户价值,每一个开发人员都希望开发功能如Windows完备的软件。以确保自己以后能够得到房子车子,所以他们总会用各种借口来为自己的过度设计做出辩解。

如果不加以控制(Deathline),我相信一个“专业”的开发团队能把记事本开发成Word。

然而,Deathline的控制粒度过大。这样会造成在临近Deathline的时候焦头烂额。


小步前进的方式,就能杜绝这一点。让开发人员的注意力都集中在了解决当前的问题,确保其在为客户创造价值而不是为自己。



小步前进的方式,也能确保我们的开发过程,是以创造最有价值的东西优先的。换言之我们在不断的解决现有的问题,无论任何时候中止开发,我们都已经创造了价值(解决了当前的问题)。

就像LZ的TDD实践中,我们用很丑陋的方式解决了问题。重构只是使得我们将来的开发成本降低,其实并没有创造当前价值。换言之就是当我们用丑陋的方式解决了问题的时候,我们其实已经可以向客户交付产品,而客户已经可以用这个产品来创造价值,或者提供更有效的反馈了。这,显然要比老赵的实践来得快得多。

  回复  引用  查看    
#12楼2009-10-21 12:15 | Shuhari      
你的方法是的确更严格的TDD,和Kent Beck的TDD例子过程基本上完全一致,我从前学TDD的时候也是这样做的。但现在我不再严格按照这个步骤了,而是自己又做了调整,为什么呢?

1. 你的实现里面第3步太小,第6步又太大。我用TDD的时候发现这个问题发生得非常频繁,“只让测试通过”的代码和“完整实现”的代码差别非常大,而第3步的实现到第6步的实现中间,很难测试新的用例来把这个过程细化。所以我的方法是:如果有显而易见的实现,就写下来。Kent Beck也说过,这样做是不违反TDD的。

2. 我不会写像你这样写完一个很大的方法再来重构,这样一个大函数且存在多个抽象层次的话,一次写完就不太实际,重构更加辛苦。我的方法是从面到点,逐个击破,这样效果最好。

  回复  引用  查看    
#13楼2009-10-21 12:18 | Teddy's Knowledge Base      
一直对TDD的本质不够清楚,楼主讲的很清楚,学习了。不过我还有个疑问,如果说TDD关注的是目前的需求,而我的实现一般来讲还会考虑将来的扩展性,也意味着必然有不少超出现有需求的实现,这些实现的测试代码,在什么时机写最好呢?

我可不可以这样理解,其实,TDD和UnitTest完全是两个概念。TDD关注已有的需求,用例。而UnitTest,甚至Test First关注的是实现代码的质量。

  回复  引用  查看    
#14楼2009-10-21 12:20 | Ivony...      
引用Shuhari:
你的方法是的确更严格的TDD,和Kent Beck的TDD例子过程基本上完全一致,我从前学TDD的时候也是这样做的。但现在我不再严格按照这个步骤了,而是自己又做了调整,为什么呢?

1. 你的实现里面第3步太小,第6步又太大。我用TDD的时候发现这个问题发生得非常频繁,“只让测试通过”的代码和“完整实现”的代码差别非常大,而第3步的实现到第6步的实现中间,很难测试新的用例来把这个过程细化。所以我的方法是:如果有显而易见的实现,就写下来。Kent Beck也说过,这样做是不违反TDD的。

2. 我不会写像你这样写完一个很大的方法再来重构,这样一个大函数且存在多个抽象层次的话,一次写完就不太实际,重构更加辛苦。我的方法是从面到点,逐个击破,这样效果最好。



或者说你也和老赵一样,步子太大了。。。。呵呵


我觉得这段话解答了这个问题,也是我一直困惑的问题。


但是对于复杂的程序,能力再强也不可能一下子全部设计出最终结构来,还是需要重构迭代的,这个时候就由回归到TDD的范畴来了,当然他可能要重构的时候又一下子设计好了结构^_^,对他的作用只好又减半了。

不过TDD并不是只针对个人的,他也是对团队的,没错老赵是设计好了最终程序结构,但是实现可能是团队其它人做的,这个时候,用例就体现了需求说明的作用,同时保验证实现人员代码的最终结果。

我认为TDD应该是Requiredment -> Test -> Development,关键是对把需求和开发联系起来,所以也可以说这个是个消化需求的方法论,用小步前进有利于充分的理解需求,同时也可以帮助设计。当我们一下子设计不出来最终结果的时候,可以利用这种方法,帮助自己思考。




或者更通俗点说,TDD是一种可以弥补把控能力不强的方法。

如果你对项目把控能力已经很强(你有很多经验和强大的抽象能力),TDD对你的帮助就会比较小。

  回复  引用  查看    
#15楼2009-10-21 12:35 | Ivony...      
引用浪子:
@Ivony
其实TDD关注的也是客户的需求,它不会未来可能出现的需求做用例,只为目前看到的需求做用例。如果你把这点和设计练习起来,进行适度的设计(而不是过度设计),它的好处应该还是可以体现出来。

总得来说,TDD对吉日的作用远大于对老赵的作用,这就是开发中个人对个人追求目标定位不一样产生不一样的效果。

其实TDD的目标只是“Clean Code That Work“,它更关注的是客人的价值实现(业务需求),而不是代码自身/程序员自身的价值。

这样子看来TDD是很适合商业软件开发的。



结合一下,我们是不是可以可以认为(老实说我真的觉得这样说会不会太露骨):

当我们已经是开发人员的顶层的时候,我们并不需要TDD来约束我们,因为我们要实现自己的价值。只有在我们很难去实现客户的价值的时候,或者说客户逼着我们必须用最小成本实现的时候。TDD才是有意义的。
但对于我们所管理的人员,TDD可以让他们多干活少搞科研。

结论就是,如果你打算开一个软件公司,用TDD去约束“大牛”,这样可以避免他搞科研。
或者说如果要任何一个人去自觉自愿的TDD,可能都会感觉很不舒服。除非是已经像“吉日”那样看破世事。拿钱干活,交货走人。。。
或者,你处于按工计酬的位子上。。。。。

  回复  引用  查看    
#16楼[楼主]2009-10-21 13:36 | 浪子      
引用Shuhari:
你的方法是的确更严格的TDD,和Kent Beck的TDD例子过程基本上完全一致,我从前学TDD的时候也是这样做的。但现在我不再严格按照这个步骤了,而是自己又做了调整,为什么呢?

1. 你的实现里面第3步太小,第6步又太大。我用TDD的时候发现这个问题发生得非常频繁,“只让测试通过”的代码和“完整实现”的代码差别非常大,而第3步的实现到第6步的实现中间,很难测试新的用例来把这个过程细化。所以我的方法是:如果有显而易见的实现,就写下来。Kent Beck也说过,这样做是不违反TDD的。

2. 我不会写像你这样写完一个很大的方法再来重构,这样一个大函数且存在多个抽象层次的话,一次写完就不太实际,重构更加辛苦。我的方法是从面到点,逐个击破,这样效果最好。

TDD有三个最突出的方法:伪实现,显明实现和三角法.我觉得这就是对你说的方法的总结.

是的3步太小,因为他是伪实现,但是其实对于这个需求,我们可以直接得出显明实现,并应用三角法泛化参数. 这个步子大小是由个人来决定的.只不过思考的过程是一样的,之时没有按步写出来而已.

关于你说的很大的方法,我觉得那个是最基础最丑陋的最显明的实现了不是嘛? 再优秀再复杂的设计都可以从最基础最简陋的最显明的实现重构得到是不?

我们都认可一句话"优秀的程序是重构出来的",我们同样也认可"重构是在不改变当前可见行为的情况下,整理代码". 所以我不觉得那样子的写法不实际,更辛苦.反而我觉得那是对应的最简单,最直接的写法,同样也可以重构出你实现设计好的结构出来.


引用Teddy's Knowledge Base:
一直对TDD的本质不够清楚,楼主讲的很清楚,学习了。不过我还有个疑问,如果说TDD关注的是目前的需求,而我的实现一般来讲还会考虑将来的扩展性,也意味着必然有不少超出现有需求的实现,这些实现的测试代码,在什么时机写最好呢?


其实我上面忽略了一个很重要的关键点.
"在TDD里,任何超前于需求的设计都是过度设计". 这样子来看待TDD,我们可能就会得到更合适的使用场景.

对于你说的未来的扩展性,如果要套用TDD做开发的话,你可以把他提前变成当前的需求,由需求分析得出测试用例,继而引导你的开发.我觉得所有的时机都来之需求还未完成之时.

引用Teddy's Knowledge Base:
我可不可以这样理解,其实,TDD和UnitTest完全是两个概念。TDD关注已有的需求,用例。而UnitTest,甚至Test First关注的是实现代码的质量。


我也认同这样子的理解,TDD是测试用例(我可不可说这是User Case?)驱动,不是单元测试驱动.一个很重要的表现是他们的完整性是不一样的.


引用Ivony...:
结合一下,我们是不是可以可以认为(老实说我真的觉得这样说会不会太露骨):

当我们已经是开发人员的顶层的时候,我们并不需要TDD来约束我们,因为我们要实现自己的价值。只有在我们很难去实现客户的价值的时候,或者说客户逼着我们必须用最小成本实现的时候。TDD才是有意义的。
但对于我们所管理的人员,TDD可以让他们多干活少搞科研。

结论就是,如果你打算开一个软件公司,用TDD去约束“大牛”,这样可以避免他搞科研。
或者说如果要任何一个人去自觉自愿的TDD,可能都会感觉很不舒服。除非是已经像“吉日”那样看破世事。拿钱干活,交货走人。。。
或者,你处于按工计酬的位子上。。。。。


认可这样子的说法,TDD关注商业价值,对于个人价值,只在于无法设计出理想结果的时候,用来帮助自己理清思路,可以使用第一个用例开始,有点到面的引申出各种可能的需求,然后加以实现,重构.

我们应该回顾下TDD的好处是什么

引用抄自网络^_^...:
(1)完工时完工。表明开发人员可以很清楚的看到自己的这段工作已经结束了,而传统的方式很难知道什么时候编码工作结束了。

(2)全面正确的认识代码和利用代码,而传统的方式没有这个机会。

(3)开发小组间降低了交流成本,提高了相互信赖程度。

(4)避免了过渡设计。

(5)系统可以与详尽的测试集一起发布,从而对程序的将来版本的修改和扩展提供方便。

(6)逃避了设计角色。对于一个敏捷的开发小组,每个人都在做设计。

(7)大部分时间代码处在高质量状态,100%的时间里成果是可见的。

(8)由于可以保证编写测试和编写代码的是相同的程序员,降低了理解代码所花费的成本。

(9)为减少文档和代码之间存在的细微的差别和由这种差别所引入的Bug作出杰出贡献。

(10)在预先设计和紧急设计之间建立一种平衡点,区分哪些设计该事先做、哪些设计该迭代时做提供了一个可靠的判断依据。

(12)发现比传统测试方式更多的Bug。

概括起来,测试驱动开发的基本过程如下:

(1) 明确当前要完成的功能。可以记录成一个 TODO 列表。

(2) 快速完成针对此功能的测试用例编写。

(3) 测试代码编译不通过。

(4) 编写对应的功能代码。

(5) 测试通过。

(6) 对代码进行重构,并保证测试通过。

(7) 循环完成所有功能的开发。

  回复  引用  查看    
#17楼2009-10-21 14:30 | Ivony...      
我想LZ的文章中我学到了一个很重要的东西,就是测试(或者说需求)的演进。需求得到后,我们应该先做出最简单的测试用例,然后不断的利用三角法新增测试用例(演进)。在这个过程中,驱动着我们的开发。

我似乎看到了一个非常好的TDD的应用场景。

  回复  引用  查看    
#18楼[楼主]2009-10-21 15:57 | 浪子      
@Ivony...
引用Ivony...:
我想LZ的文章中我学到了一个很重要的东西,就是测试(或者说需求)的演进。需求得到后,我们应该先做出最简单的测试用例,然后不断的利用三角法新增测试用例(演进)。在这个过程中,驱动着我们的开发。

我似乎看到了一个非常好的TDD的应用场景。


是的,我认为这才是"驱动"的本质.所以我说它是消化需求,引导设计的一种方法论.不仅仅是开发层面上的.