Hello World
Spiga

我的TDD实践:可测试性驱动开发(下)

2009-10-19 08:48 by 老赵, 18523 visits

上一篇文章里,我谈到自己在采用传统TDD方式进行开发时感到有些尴尬,最后不得不放弃这种先写测试再写代码最后重构的方式。不过我还是非常注重单元测试的实践,慢慢发现自己的做法开始转向另一种TDD方式,也就是“可测试性驱动开发”。简单的说,我现在采取的做法是,先开发,再测试,一旦发现产品代码不太容易测试,则将其重构为容易测试的代码。我发现,这种时刻注重可测试性的开发方式,其最终也能够得到质量较高的代码。例如,它和SOLID原则也颇为融洽。上次谈的比较理论,而这次我便通过一个简单功能的开发过程,来表现我的思维方式及常用做法。

任务描述

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

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; }
}

SearchCriteria中包含三个条件,一是复杂类型的Price条件,二是字符串类型的Keywords条件,三是一个Color枚举类型。作为查询条件,它总是需要在URL中表示出来的,而表现方式则是最近在《趣味编程:从字符串中提取信息》中定义的做法。例如,如果是这样的URL:

/keywords-hello%20world--price-100-200--color-black-red

它表示的便是这样的条件:

  • 价格为100到200之间
  • 关键字为“hello world”(注意URL转义)
  • 颜色为黑或红(使用Color.Black | Color.White表示)

而最终,我要使用“可测试性驱动开发”来实现的便是这个方法:

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

那么,我又会怎么做呢?

实现步骤

其实这也是个比较简单的功能,于是我一开始便用最直接的方式进行开发:

public class SearchCriteriaBinder : IModelBinder
{
    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        var modelName = bindingContext.ModelName;
        var rawValue = bindingContext.ValueProvider[modelName].RawValue;

        var text = HttpUtility.UrlDecode(rawValue.ToString());
        var tokenGroups = this.Tokenize(text);
        ...
    }

    private List<string[]> Tokenize(string text)
    {
        ...
    }
}

这个Model Binder会从Value Provider中得到Model Name(一般Action参数的名称,在这里不是重点)所对应的rawValue,经过了URL Decode之后便得到了text,它是一个带有信息的字符串,也便是《趣味编程》所要解析的对象。如上面的例子,text便是:

keywords-hello world--price-10-20--color-black-red

请注意,原本在URL中表示为%20的字符,已经被URL Decode为一个空格。在得到text之后,我便要将其拆分为一个List<string[]>对象,这便是分割好的结果。拆分字符串的逻辑比较复杂,因此我将其提取到一个独立的Tokenize方法中去。于是我接下来就开始实现Tokenize方法了,写啊写,写完了。但是,我到底写的正不正确?我不知道。我唯一知道的东西是,这个逻辑不简单,我需要测试一下才放心。因此,在继续其他工作之前,我想要为它写一些单元测试。

这就是涉及到一个问题,我们该如何为一个私有方法作单元测试呢?我以前也想在博客上讨论这个问题,但是最终不知为何没有进行。我的看法是,如果设计得当,每个类的职责单一,应该不会出现需要进行单元测试的私有方法。如果一个私有方法需要测试,那么说明它的逻辑相对较为复杂,而且有独立的职责,应该将其提取到外部的类型中。例如在这里,Tokenize方法便值得我这样么做——因为我想要单元测试。于是我提取出一个Tokenizer抽象,以及一个默认的逻辑实现:

internal interface ITokenizer
{
    List<string[]> Tokenize(string text);
}

internal class Tokenizer : ITokenizer
{
    public List<string[]> Tokenize(string text)
    {
        ...
    }
}

我现在便可以对Tokenizer进行充分的单元测试,以确保它的功能满足我的要求。测试完成后,我就对它完全放心了。而此时,我的SearchCriteriaBinder便会直接使用Tokenizer对象,而不是内部的私有方法——当然,是基于抽象来的:

public class SearchCriteriaBinder : IModelBinder
{
    public SearchCriteriaBinder()
        : this(new Tokenizer()) { }

    internal SearchCriteriaBinder(ITokenizer tokenizer)
    {
        this.m_tokenizer = tokenizer;
    }

    private readonly ITokenizer m_tokenizer;

    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        var modelName = bindingContext.ModelName;
        var rawValue = bindingContext.ValueProvider[modelName].RawValue;

        var text = HttpUtility.UrlDecode(rawValue.ToString());
        var tokenGroups = this.m_tokenizer.Tokenize(text);
        ...
    }
}

原本由私有的Tokenize方法负责的逻辑,现在已经委托给ITokenzier对象了,而这个对象可以在构造SearchCriteriaBinder对象时通过构造函数提供。请注意,提供ITokenizer对象的构造函数访问级别是internal,也就是说,它可以被单元测试代码所访问(通过InternalVisibleToAttribute),但是无法被另一个程序集的使用方调用。也就是说,经过重构的SearchCriteriaBinder,它的可测试性提了,但是对外的表现却没有丝毫变化。

经过简单思考,便可以发现这一简单的改变其实也较为满足SOLID原则中的一部分:

  • 单一职责:SearchCriteriaBinder职责很单一,解析工作交由同样职责单一的Tokenizer进行。
  • 依赖注入:这里使用了构造函数注入的方式,SearchCriteriaBinder对Tokenizer的依赖不是写死在代码里的。
  • 接口分离:SearchCriteriaBinder并不直接访问Tokenizer,而是通过一个抽象(ITokenizer)使用的。

这便是我常用的“可测试性驱动开发”,我一开始只是按照惯例直接实现BindModel方法,然后发现一个需要测试的私有方法,因此为了提高可测试性,我将部分功能提取到独立的Tokenizer对象中去。在实际开发过程中,我也可能是直接在脑子里进行简单的分析,然后直接发现我们的确需要一个Tokenizer方法(这点想象应该不难),于是直接实现ITokenzier接口,Tokenizer类以及单元测试。这样,可能SearchCriteriaBinder从一开始就会变成目前的样子。不过更多的情况,的确是写着写着,为了“可测试性”而进行的重构。为了说明这个“思路”,我接下来还是使用“编写代码——尝试测试而不能——重构”的方式来进行开发。

好,我已经拆分成功了,也就是得到了一个List<string[]>对象。接下来,我要读取其中的数据,将其转化为一个SearchCriteria对象:

public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
    ...
    var tokenGroups = this.m_tokenizer.Tokenize(text);

    return this.Build(tokenGroups);
}

private SearchCriteria Build(List<string[]> tokenGroups)
{
    var fieldTokens = tokenGroups.ToDictionary(
        g => g[0].ToLowerInvariant(),
        g => g.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))
    {
        ...
    }

    return searchCriteria;
}

在BindModel方法中得到了tokenGroups之后,便交由Build方法进行SearchCriteria对象的构建。首先,我先将List<string[]>对象转化为“字段”和“值”的对应关系,这样我们便可以使用keywords、price等字符串获取数据(也就是一个List<string>对象),并生成SerachCriteria各属性所需要的值了。在这里,我们这一切都放在Build方法中的几个if里进行,但这很显然不是容易单元测试的方法。要知道,这里的代码看上去容易,但事实上每个if里的逻辑其实并不仅仅如此。例如,在输入不合法的情况下是容错,还是抛出异常?如果Min大于Max的情况下,是否直接将其交换再继续处理?因此,其实在每个if之中还会有if,还会有for等复杂的逻辑。对于这样的逻辑,我想要单元测试

于是,我为List<string>到特定对象的转换操作也定义一个抽象:

public interface IConverter
{
    object Convert(List<string> values);
}

public class KeywordConverter : IConverter
{
    public object Convert(List<string> values)
    {
        return values[0];
    }
}

public class PriceRangeConverter : IConverter
{
    public object Convert(List<string> values)
    {
        return new PriceRange
        {
            Min = float.Parse(values[0]),
            Max = float.Parse(values[1])
        };
    }
}

public class ColorConverter : IConverter
{ 
    ...
}

在这里,我为每个字段定义了一种转化器(而在实际开发过程中,我们可能也会为“每种类型”定义一个)。每个转化器对象均可独立的进行单元测试,其中复杂的边界条件,错误判断等等都是测试的目标。待几种转换器测试完毕,我们便可以重构SerachCriteriaBinder的Build方法:

private SearchCriteria Build(List<string[]> tokenGroups)
{
    var fieldTokens = tokenGroups.ToDictionary(
        g => g[0].ToLowerInvariant(),
        g => g.Skip(1).ToList());

    var searchCriteria = new SearchCriteria();

    List<string> values;
    if (fieldTokens.TryGetValue("keywords", out values))
    {
        searchCriteria.Keywords = (string)this.GetConverter("keywords").Convert(values);
    }

    if (fieldTokens.TryGetValue("price", out values))
    {
        searchCriteria.Price = (PriceRange)this.GetConverter("price").Convert(values);
    }

    if (fieldTokens.TryGetValue("color", out values))
    {
        searchCriteria.Colors = (Color)this.GetConverter("color").Convert(values);
    }

    return searchCriteria;
}

private IConverter GetConverter(string field)
{
    // 使用if ... else或是字典
}

又想测试GetConverter方法了,怎么办?那就还是把GetConverter这部分逻辑从外部注入吧——哎,难道还要我写一个IConverterFactory接口和ConverterFactory类吗?也不一定,我们还是用“轻量”些的方法吧:

public class SearchCriteriaBinder : IModelBinder
{
    ...

    public SearchCriteriaBinder()
        : this(new Tokenizer(), GetConverter) { }

    internal SearchCriteriaBinder(ITokenizer tokenizer, Func<string, IConverter> converterGetter)
    {
        this.m_tokenizer = tokenizer;
        this.m_getConverter = converterGetter;
    }

    private readonly ITokenizer m_tokenizer;
    private readonly Func<string, IConverter> m_getConverter;

    internal static IConverter GetConverter(string field)
    {
        // 使用if ... else或是字典
    }

    private SearchCriteria Build(List<string[]> tokenGroups)
    {
        ...

        if (fieldTokens.TryGetValue("keywords", out values))
        {
            searchCriteria.Keywords = (string)this.m_getConverter("keywords").Convert(values);
        }

        ...
    }
}

这一次,我使用了委托对象的方式注入一段逻辑,它其实也是我们可以使用的一种方式。这样,我们便可以为GetConverter方法作独立的单元测试了。不过,我这里使用委托也有点“展示”的意味在里面,在实际开发过程中,可能我还是会使用ConverterFactory,这对我来说更“正规”一些。这种“接口”与“实现”分离的做法,除了能够独立测试之外,还有一个目的就是为了在测试Build方法时不依赖GetConverter的实现,也意味着不依赖PriceRangeConverter,KeywordConverter的实现等等,因为我们在测试Build方法时,可以提供一个针对测试的GetConverter方法逻辑,返回一些IConverter的Mock或Stub对象。这样,Build方法也就非常独立,不依赖外部实现了。

没错,我们想要独立测试Build方法,但是现在SearchCriteriaBinder还不允许我们这么做。那么,继续重构吧:

internal interface ISearchCriteriaBuilder
{
    SearchCriteria Build(List<string[]> tokenGroups);
}

internal class SearchCriteriaBuilder : ISearchCriteriaBuilder
{
    internal SearchCriteriaBuilder() : this(GetConverter) { }

    internal SearchCriteriaBuilder(Func<string, IConverter> converterGetter)
    {
        this.m_getConverter = converterGetter;
    }

    internal static IConverter GetConverter(string field)
    {
        // 使用if ... else或是字典
    }

    private readonly Func<string, IConverter> m_getConverter;

    public SearchCriteria Build(List<string[]> tokenGroups)
    {
        ...
    }
}

把Build的逻辑独立提取成类之后,自然需要让SearchCriteriaBinder使用SearchCriteriaBuilder:

public class SearchCriteriaBinder : IModelBinder
{
    public SearchCriteriaBinder()
        : this(new Tokenizer(), new SearchCriteriaBuilder()) { }

    internal SearchCriteriaBinder(ITokenizer tokenizer, ISearchCriteriaBuilder builder)
    {
        this.m_tokenizer = tokenizer;
        this.m_builder = builder;
    }

    private readonly ITokenizer m_tokenizer;
    private readonly ISearchCriteriaBuilder m_builder;

    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        var modelName = bindingContext.ModelName;
        var rawValue = bindingContext.ValueProvider[modelName].RawValue;

        var text = HttpUtility.UrlDecode(rawValue.ToString());
        var tokenGroups = this.m_tokenizer.Tokenize(text);

        return this.m_builder.Build(tokenGroups);
    }
}

至此,我们的SearchCriteriaBinder便开发完成了。您可以访问http://gist.github.com/212644浏览最终的结果。

总结

以上便是一个完整的SearchCriteriaBinder的开发过程。可以发现,虽然我们的目标只有SearchCriteriaBinder一个,它也是唯一在外部可以访问到的类型(即public),但是我们这里总共出现了9个接口或是类。整个SearchCriteriaBinder是通过它们的协作完成的。9个看上去很多,但其实它们之间并没有复杂的交互,你会发现每个类本身只是和另外1至2个抽象有联系,它们也没有复杂的依赖关系。确切地说,我只是把它们拆成了一个个独立的小功能而已。

拆成小功能,只是为了进行独立的,细致的单元测试。也就是说,我的每一次重构,每一次拆分,目的都是为提高“可测试性”(我把它们都标红了),因此它是“可测试性驱动开发”。在为某个类进行单元测试的时候,也不会依赖其他类的具体实现,因为所有的类访问的都是抽象,我们只需要为这些抽象创建Mock——其实只是Stub就可以了。例如:

  • 测试SearchCriteriaBinder时,为ITokenizer和ISearchCriteriaBuilder创建Stub。
  • 测试SearchCriteriaBuilder时,为Func<string, IConverter>委托提供实现(也就是个Stub)。

这样,SearchCriteriaBinder的单元测试出错了,那么有问题的一定是SearchCriteriaBinder的实现,而不会是因为Tokenizer实现出错而造成的“连锁反应”。至于其他的类,都只是最简单的“工具类”,没有比它们更容易进行单元测试的东西了。

与传统的TDD相比,我常用的这种“可测试性驱动开发”使用的还是先开发,再测试的做法。在开发的时候,我们使用传统的设计方式,可能设计的只是一套类库/框架对外的表现。例如,我们在开发ASP.NET MVC应用程序时,知道我们需要一个SearchCriteriaBinder来生成Action的参数。于是,这个程序集的职责只是暴露出这个Binder而已。在具体实现这个Binder的过程中,我们也是用非常直接的开发方式,只是会时不时地关注“可测试性”。

“时不时”地关注,这点并不夸张。因为我在实际开发过程中,不会编写大段的逻辑再进行测试,而是写完一段之后(如Tokenize方法)我就会担心“这部分写的到底对不对”。于是,我不会等整个SearchCriteriaBinder实现完成便会提取出Tokenizer,实现并测试。这么做,也可以保证我的开发过程是渐进的,每一步都是走踏实的。使用这种方法,似乎也可以得到TDD的优势:

  • 得到许多测试
  • 模块化
  • 对重构和修改代码进行保护
  • 框架内部设计的文档
  • ……

如果要使用传统的TDD开发SearchCriteriaBinder的话,可能就需要先设计一个输入字符串,然后为它直接设计一个输出。此时,在这个测试中就要考虑许许多多的东西了,例如字符串的拆分,数据的转化,以及转化的各种边界情况等等。事实上,我认为如果不对SearchCriteriaBinder进行分割,是根本无法做到细致的完整的单元测试的。因此,即便是传统的TDD方式,最终也一定会将SearchCriteriaBinder分割成更小的部分。

我认为,使用传统的TDD方式最终得到的结果和“可测试性驱动开发”是很接近的——我是指产品代码。其中的区别可能是使用TDD的方式会有更多更细小的测试,也就是那些被人认为是非常stupid的测试。在开发一些“工具类”的时候,我们很容易想到此类细小的测试,但是大类就不一定了。在面向对象的方式进行开发时,涉及到更多的可能是类之间的交互。这时候“测试驱动”的思维(对我来说)就有些奇怪了。因此,我会选择先进行开发,然后重构成易于测试的形式。

“可测试性驱动开发”和传统的TDD也是不矛盾的,它们完全可以混合使用。例如在开发SearchCriteriaBinder时,我也不会将Tokenize这个私有方法真正实现完毕之后才提取出Tokenizer。因为,我其实很快(甚至是在脑子里“写代码”时)就会意识到Tokenize方法是有独立意义的,是需要单元测试的。因此,我会早早地定义ITokenizer接口,然后在开发Tokenizer这个工具类的时候,便使用传统TDD的方式进行。

这样看来,似乎我们也可以这么说:“可测试性驱动开发”是偏向于“设计”的(毕竟“可测试性”是设计出来的),而传统TDD则更偏向于“实现”。

Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

69 条回复

  1. Leon Weng
    *.*.*.*
    链接

    Leon Weng 2009-10-19 08:58:00

    和老赵同时发文。坐个沙发 嘿嘿

  2. Leon Weng
    *.*.*.*
    链接

    Leon Weng 2009-10-19 09:00:00

    文章确实少了,真灵异。

  3. 家中慢步
    *.*.*.*
    链接

    家中慢步 2009-10-19 09:11:00

    呃。技术性太强, 更喜欢看老赵写随笔, 有丰富思想的人

  4. killkill
    *.*.*.*
    链接

    killkill 2009-10-19 09:15:00

    赞!!
    极具指导意义。

  5. 亚历山大同志
    *.*.*.*
    链接

    亚历山大同志 2009-10-19 09:19:00

    其实很早前就打算实践一下TDD,但是最麻烦的是很多时候感觉要测试的东西都有两个开口,一个是用户输入,另外一个就是数据库,尤其是数据库特别麻烦,比如一个功能修改后是否正常需要在数据库里预先准备一大堆数据。。。。。。

  6. 老赵
    admin
    链接

    老赵 2009-10-19 09:21:00

    @亚历山大同志
    单元测试是不应该依赖外部资源,比如数据库的,否则就变集成测试了,TDD也是一样啊。

  7. 横刀天笑
    *.*.*.*
    链接

    横刀天笑 2009-10-19 09:32:00

    Good,这篇文章看着很爽
    特别是下面这段,曾经有一个同学跟我讨论,如何对私有方法进行测试呢?想了半天说私有方法不需要测试,然后他说:那私有方法如果较复杂需要测试呢?我就卡壳了,今天看到这里有点豁然开朗的感觉。

    这就是涉及到一个问题,我们该如何为一个私有方法作单元测试呢?我以前也想在博客上讨论这个问题,但是最终不知如何没有进行。我的看法是,如果设计得当,每个类的职责单一,应该不会出现需要进行单元测试的私有方法。如果一个私有方法需要测试,那么说明它的逻辑相对较为复杂,而且有独立的职责,应该将其提取到外部的类型中。例如在这里,Tokenize方法便值得我这样么做——因为我想要单元测试。于是我提取出一个Tokenizer抽象,以及一个默认的逻辑实现:


  8. 妖居
    *.*.*.*
    链接

    妖居 2009-10-19 09:39:00

    可测试的代码都是可移植性很好的,或者说可替换性很好的,因为至少需要替换上用于单元测试的Stub或者Mock对象,无形当中增加了代码的复杂度。我的体会是,这种情况下需要平衡。当然在时间和预算充足(或者说合理)的前提下,尽量多的测试和尽量好的设计当然是我的第一选择。但是如果遇到了时间和预算的制约,我则会倾向于为最核心、最复杂的逻辑做单元测试;而对于其他的相对简单的逻辑,可能手动测试更快也更方便。

  9. 麒麟.NET
    *.*.*.*
    链接

    麒麟.NET 2009-10-19 09:41:00

    现在你的首页直接无法访问了……

  10. 老赵
    admin
    链接

    老赵 2009-10-19 09:44:00

    难道是博客园欺负我人太好么……

  11. Leon Weng
    *.*.*.*
    链接

    Leon Weng 2009-10-19 10:16:00

    麒麟.NET:现在你的首页直接无法访问了……


    刚才又试了一下 可以访问

  12. 博客园团队
    *.*.*.*
    链接

    博客园团队 2009-10-19 10:25:00

    "从昨天晚上开始,我的博客(看这里)就少了半年的文章,拜托博客园管理层帮忙看看。"
    非常抱歉!这个模板的随笔列表排序出现了问题。
    现在好了。

  13. Nick Wang (懒人王)
    *.*.*.*
    链接

    Nick Wang (懒人王) 2009-10-19 10:25:00

    不管用什么,目的是做出好的设计,从而解决要解决的问题。而设计是由开发人员的创造出来的,而不是TDD或者xxD。就好像用VS和Notepad都能写代码,VS可能能帮助你写得更快更好,但是不能替你写,最终还是要你自己会写才行。

    所以说xxD都是VS一样的工具,你喜欢、能从中得到好处,你就用;觉得不喜欢,换一种工具,只要能很好的完成任务就没问题。

  14. oec2003
    *.*.*.*
    链接

    oec2003 2009-10-19 10:28:00

    新浪的blog有时也会出现这种问题,首页的文章列表会莫名其妙的少了

  15. 阿不
    *.*.*.*
    链接

    阿不 2009-10-19 10:28:00

    从老赵的这篇文章能够体会到,我自己平常很难在项目中实践单元测试开发的一个重要的症结在于设计和抽象能力的不足。
    此外,与亚历山大同志存在着相同的疑惑就是,在很多情况下,对于具有一定业务性的功能,需要得到各方面资源的配合和准备才能完成的测试,这时候如何来实践单元测试。如果这种情况不进行单元测试,如何来保证项目中,业务逻辑部分代码的正确性?
    再往具体点说,我们使用MVC在开发时,我们可能会对Controller进行单元测试,那么Controller势必就是一个业务功能点,那么就不可避免的与数据库等资源交互。这种情况如何来隔离外部资源对单元测试的影响?

  16. Nick Wang (懒人王)
    *.*.*.*
    链接

    Nick Wang (懒人王) 2009-10-19 10:39:00

    @阿不
    如果Controller包含业务,或者比较复杂,可以抽取出service层,对service进行业务方面的测试,对controller进行页面级逻辑测试。如果需要访问数据库,创建接口,用mock模拟数据访问来测试service。如果离了数据库就无法测试业务逻辑,说明业务逻辑在数据库中,那么应该测试sp或者数据库。

  17. Nick Wang (懒人王)
    *.*.*.*
    链接

    Nick Wang (懒人王) 2009-10-19 10:41:00

    @阿不
    所谓的需要多方面配合的测试,其实就是集成问题,应该进行集成测试。单元测试不是万金油。
    如果集成中还有逻辑,那就mock掉所有的集成点,集中测试这一点逻辑。
    这里指的逻辑可能只是一个if(xxx)invoke service1 else invoke service2 这样的东西,也可能会更复杂一些。

  18. 炭炭
    *.*.*.*
    链接

    炭炭 2009-10-19 10:48:00

    MVC 没玩过,所以不好具体的分析什么。
    TDD是设计的意思是站在 类库使用者 的角度来构建类和方法的设计。
    将好比说你写了一个类库然后给别人去用,如果别人不看任何文档,不经任何培训就能明白你的类库怎么用,完成他的任务,说明你的设计是很成功的。
    所以说你先站在别人的角度写调用你类库的代码,在写相应的类。TDD就是把这个条用拓展为一个全面的测试,把各种掉用情况都考虑到。
    私有方法是不需要测试的。

  19. 小No
    *.*.*.*
    链接

    小No 2009-10-19 10:50:00

    阿不:
    从老赵的这篇文章能够体会到,我自己平常很难在项目中实践单元测试开发的一个重要的症结在于设计和抽象能力的不足。
    此外,与亚历山大同志存在着相同的疑惑就是,在很多情况下,对于具有一定业务性的功能,需要得到各方面资源的配合和准备才能完成的测试,这时候如何来实践单元测试。如果这种情况不进行单元测试,如何来保证项目中,业务逻辑部分代码的正确性?
    再往具体点说,我们使用MVC在开发时,我们可能会对Controller进行单元测试,那么Controller势必就是一个业务功能点,那么就不可避免的与数据库等资源交互。这种情况如何来隔离外部资源对单元测试的影响?



    如果你使用依赖注入模式的话就很好做测试,比如:
    有一个ProductController类,构造方法如下:
    public ProductController(IProductRepository productRepository)
    其中IProductRepository是数据操作层的接口,里面封装了对Product表的所有数据库操作,当你在对ProductController做单元测试时,你需要构造一个实现IProductRepository的假的Product数据访问层(例如叫:FakeProductRepository),数据是用代码在内存中模拟,而不是从数据库中读取。

    这时在你的单元测试方法中就可以使用以下代码去测试ProductController:
    IProductRepository fakeRepository = new FakeProductRepository();
    ProductController controller = new ProductController(fakeRepository);

    //对ProductController断言:
    .....

    由于FakeProductRepository的数据是用代码在内存中模拟的,这样就完全可以隔离数据库了,而且单元测试的效率会很高,上千个类似的单元测试方法也会很快执行完成。

  20. Kevin Dai
    *.*.*.*
    链接

    Kevin Dai 2009-10-19 10:52:00

    再有几天,老赵在博客园就是榜首了,名副其实啊!

  21. 老赵
    admin
    链接

    老赵 2009-10-19 10:53:00

    @炭炭
    这个案例其实和mvc没有关系,呵呵。
    如果你要开发我这个SearchCriteriaBinder,用TDD的话,我觉得几乎很难设计任何一个test case,更别说完整的测试集了。
    所以,我会一直拆分到很小的组件,然后对那些组件用TDD,拆分的过程是“可测试性”驱动的。
    对了,私有方法是不需要测试的,那么程序集中不对外暴露的类,要不要测试呢?
    所以我觉得,与其说TDD是从使用者角度的测试,我觉得还是从开发人员和“组件交互设计者”角度出发的测试更妥当一些。

  22. 老赵
    admin
    链接

    老赵 2009-10-19 10:56:00

    @小No
    差不多就是这个意思,其实就是“针对抽象”编程。
    Controller依赖的是一些抽象,而不是具体的东西。
    具体的东西会依赖数据库,但测试时Controller依赖的是抽象的Mock或Stub。

  23. 小No
    *.*.*.*
    链接

    小No 2009-10-19 11:19:00

    Jeffrey Zhao:
    @炭炭
    这个案例其实和mvc没有关系,呵呵。
    如果你要开发我这个SearchCriteriaBinder,用TDD的话,我觉得几乎很难设计任何一个test case,更别说完整的测试集了。
    所以,我会一直拆分到很小的组件,然后对那些组件用TDD,拆分的过程是“可测试性”驱动的。
    对了,私有方法是不需要测试的,那么程序集中不对外暴露的类,要不要测试呢?
    所以我觉得,与其说TDD是从使用者角度的测试,我觉得还是从开发人员和“组件交互设计者”角度出发的测试更妥当一些。



    用传统的TDD也可以很容易设计test case吧,先撇开BindModel方法,首先可以设计一个这样的test case,SearchCriteriaBinder起码包含一个方法会
    根据输入值:keywords-hello%20world--price-100-200--color-black-red 返回正确的SearchCriteria

  24. 老赵
    admin
    链接

    老赵 2009-10-19 11:40:00

    @小No
    但是这很难,不是吗?例如,如果测试Tokenzier,得到这么一串只要知道返回什么样的字符串数组就行了。
    但是如果测整个Binder,还会涉及转化类型,边界情况等等,很复杂,测试不了很细致的的。

  25. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-10-19 12:26:00

    我想,如果用“正统的TDD”的角度来看,老赵似乎犯了一个错误。所以或者说这种实践算不上“正统的TDD”。

    不过那无所谓,反正老赵也不是在XP。

    简单的说就是ITokenizer或者说ValueProvider实现的功能已经超出了老赵在文首提出的需求,如果我们要确保所有的设计都是最简单的。
    或者这就是老赵没有按照“正统的TDD”从而导致设计超出需求的问题。


    我们来看看原始的需求是什么?

    ASP.NET MVC中Model Binder的职责是根据请求的数据来生成Action方法的参数(即构建一个对象)。那么这次,我们将为负责产品搜索的Action方法提供一个SearchCriteria参数作为查询条件
    ……
    SearchCriteria中包含三个条件,一是复杂类型的Price条件,二是字符串类型的Keywords条件,三是一个Color枚举类型。

    请记住,SearchCriteria中只包含三个条件。

    换言之我们对ITokenizer的需求只应该限于,能把这三个条件Tokenizer的东西就够了。

    换言之我们需要的是SearchCriteriaTokenizer而不是ITokenizer。


    这样做下来岂不是什么积累都没有?
    是的,XP就是这样。

  26. 小No
    *.*.*.*
    链接

    小No 2009-10-19 12:29:00

    @Jeffrey Zhao
    你的意思是不再对整个Binder进行测试?
    如果这样做的话,你能确保中间没有什么忽略掉的吗?

    其实读了你这篇文章,觉得你这种测试方法或许比较适合你自己,但是未必适合所有人。

    因为,你这种方法需要开发人员有比较丰富的经验,特别是代码设计方面的,起码知道什么是好的设计。

    就按照你举的这个例子,如果我是个没什么经验的菜鸟,开始写代码的时候会把全部的代码放到BindModel的方法中,写完代码之后,这时做的单元测试就是对整个Binder进行测试。就跟传统TDD要写的单元测试一样。

    我觉得传统的TDD比较适合一般的开发人员,一开始只要专注功能的实现,不需要考虑太多设计方面的问题,既能完成工作,又能为以后的代码重构留下比较全面的单元测试。

    当然对一些比较有经验的开发人员,就算是使用传统的TDD,他一开始就可以到考虑到很多的话(例如本文例子中的Tokenize方法),那么他一开始就可以直接对Tokenize写测试代码,为以后的代码重构减轻工作量。

    最近我也在尝试TDD,也希望能找到一条比较适合自己的路。

  27. 麒麟.NET
    *.*.*.*
    链接

    麒麟.NET 2009-10-19 12:29:00

    @Ivony...
    老赵所说的确是不是测试驱动开发,而是可测试性驱动开发,此TDD非彼TDD

  28. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-10-19 12:30:00

    麒麟.NET:
    @Ivony...
    老赵所说的确是不是测试驱动开发,而是可测试性驱动开发,此TDD非彼TDD




    是的,刚在想我这回复会不会引起争端。

    不过我想我已经说了:“不过那无所谓,反正老赵也不是在XP”。
    如果一定要引起争端的话,我也没办法。

    或者说一定要旗帜鲜明的老赵一定在TDD,这是对。反之,老赵不一定在TDD,这是错。然后加上前提:“老赵一定是对的”。

    这恐怕我说啥都会引起争端。


    最近园子就是这样。

  29. 老赵
    admin
    链接

    老赵 2009-10-19 12:53:00

    @Ivony...
    在别人那里我没法控制,在我这里就别搞这些乱七八糟的了,呵呵。
    “逐条批驳”是要针对技术,但现在居然还是要打倒人,真烦,真像文革了。

  30. 老赵
    admin
    链接

    老赵 2009-10-19 12:56:00

    @Ivony...
    其实我没在搞传统TDD,只是关注可测试性罢了,没有用测试来驱动。
    至于Tokenizer这个东西……严格来说,SearchCriteriaTokenizer也是基于Tokenizer的,Tokenizer也是可复用给其他XxxTokenizer的。
    不过其实我这个案例倒也没想太多,就是顺着思路就这么下来了……

  31. 老赵
    admin
    链接

    老赵 2009-10-19 13:01:00

    @小No
    在我看来,对Binder是需要整个测试的,但是不追求非常完整,细致的测试,跑2-3个case就够了,而Tokenizer这种“单元”就需要细致的情况了。
    因为Binder是根据我的设计(比如交互方式)完成的组件,它只要把自己该做的做了就好了。我测的也是这个。
    就像Binder是不管Tokenizer如何的,它确信Tokenizer的功能是正确的。
    这样出错之后,我们也可以立即知道哪块是有问题的。其实这个需求也算是演变于我目前的项目(略为简化了一点)。
    从我感觉来说,设计一个输入然后直接测试SearchCriteria,实在很麻烦,而且难以完整阿。
    其实进行拆分,也是为了拆分条件路径,降低一段代码的圈复杂度(我居然想起这个了),这就提高可测试性了,呵呵。

  32. Colin Han
    *.*.*.*
    链接

    Colin Han 2009-10-19 13:15:00

    小No:

    阿不:
    我们使用MVC在开发时,我们可能会对Controller进行单元测试,那么Controller势必就是一个业务功能点,那么就不可避免的与数据库等资源交互。这种情况如何来隔离外部资源对单元测试的影响?



    如果你使用依赖注入模式的话就很好做测试,比如:
    有一个ProductController类,构造方法如下:
    public ProductController(IProductRepository productRepository)
    其中IProductRepository是数据操作层的接口,里面封装了对Product表的所有数据库操作,当你在对ProductController做单元测试时,你需要构造一个实现IProductRepository的假的Product数据访问层(例如叫:FakeProductRepository),数据是用代码在内存中模拟,而不是从数据库中读取。



    小No说的正是我这几天在考虑的一个问题,既然我们要构造FakeProductRepository,那么,我们能不能直接拿DB4O这样的面向对象数据库来做FakeRepository呢?
    反正我们要构造FakeProductRepository,那我们能不能在项目的前期就是用这个Fake类呢?
    不实现真正的数据访问层,直到业务逻辑稳定后,再真的实现数据库建模和数据访问层.这样可行吗?

  33. 小No
    *.*.*.*
    链接

    小No 2009-10-19 13:33:00

    @Jeffrey Zhao
    如果仅仅是因为测试用例做得不够,就把本来应该在单元测试发现的问题,扔到运行时才发现,那这个单元测试是不是做得没什么大意义呢?

    当然我也赞同,做单元测试有时真的很难做到非常完整。你说对Binder只跑2,3个case,如果这两2,3个case能包含大部分的情况(80%以上),那么我认为也足够了。那么就用这个2,3个case先写测试代码。通过了,到时重构的时候还是会提取Tokenizer的,到时再对Tokenizer进行详细的测试也是可以的啊。

  34. 小No
    *.*.*.*
    链接

    小No 2009-10-19 13:54:00

    @Colin Han
    我觉得使用DB4O不太好,那样跟直接使用SQL SERVER的效果一样。

    因为在我看来,使用FakeRepository主要的好处是
    1. 提高测试方法的执行效率。DB40始终是一个数据库,打开关闭连接都是需要时间的。

    2. 隔离外部资源对单元测试的影响,在代码修改之前,单元测试方法无论重复运行多少次,结果都是一样的。而使用DB4O的话,里面数据的变化有可能会影响其他测试方法,而在用代码在内存里模拟数据的话,就不会出现这种情况。

  35. 老赵
    admin
    链接

    老赵 2009-10-19 13:54:00

    @小No
    我拆分的目的就是为了做细致的单元测试,然后在不要在集成测试时暴露问题啊。

  36. 老赵
    admin
    链接

    老赵 2009-10-19 13:56:00

    @Colin Han
    FakeRepository也尽量不要依赖外部,使用自己构造的对象。

  37. 老赵
    admin
    链接

    老赵 2009-10-19 13:57:00

    @小No
    其实我想,如果每次测试时,在db4o里放入自己准备的对象就行了——只是的确还是花时间。

  38. 小No
    *.*.*.*
    链接

    小No 2009-10-19 13:59:00

    @Jeffrey Zhao
    老赵,我想请教你几个问题。
    1. 你们项目做集成测试的时候是怎么做的?
    是首先编写测试用例,然后直接到UI界面上去做相关操作吗?

    2. 数据访问层的单元测试,你们做不做啊?

  39. 老赵
    admin
    链接

    老赵 2009-10-19 14:06:00

    @小No
    1、如果是传统意义上“集成测试”,那就是……一帮子女人点来点去。
    2、不做,不知道怎么做,所以我也越来越不喜欢复杂的数据访问层。

    // 你们做数据访问层测试吗?

  40. 小No
    *.*.*.*
    链接

    小No 2009-10-19 14:10:00

    Jeffrey Zhao:
    @小No
    其实我想,如果每次测试时,在db4o里放入自己准备的对象就行了——只是的确还是花时间。



    如果只做查询的单元测试或许可以,如果涉及到增、删、改操作,那不是每次都要在方法结束前,还原数据?否则对下一个测试方法就有可能有影响。

    不过我最近在做数据访问层的单元测试时,发现了一个比较好的办法,xUnit.Net的扩展里有个AutoRollback的Attribute,自动对测试方法添加事务,在测试方法结束后,会对数据库进行Rollback。也就是说无论你在测试方法里对数据库进行什么操作,对其他测试方法都不会产生影响。

  41. 老赵
    admin
    链接

    老赵 2009-10-19 14:15:00

    @小No
    每个测试都使用新建的文件,或者就在内存里建个对象。

  42. 小No
    *.*.*.*
    链接

    小No 2009-10-19 14:25:00

    Jeffrey Zhao:
    @小No
    1、如果是传统意义上“集成测试”,那就是……一帮子女人点来点去。
    2、不做,不知道怎么做,所以我也越来越不喜欢复杂的数据访问层。

    // 你们做数据访问层测试吗?


    我们目前还是做数据访问层测试,起码可以减少点集成测试时发现的问题

  43. Colin Han
    *.*.*.*
    链接

    Colin Han 2009-10-19 14:26:00

    所以说,数据访问层还是应该尽量的薄。

    如果是这样,那还需要数据访问层吗?我能想到的数据访问层的两个用途:1.持久化;2.封装基本业务逻辑;3.安全性控制。

    第一条,似乎已经被NH支持的很好了,似乎已经不需要了。(呵呵,我不认为领域实体属于数据访问层)
    后面两条又很难进行单元测试,因此需要很薄很薄。那么有没有可能让他们成为一个业务无关的类库。从而消除这一层。

    呵呵,说到最后,我都不知道我要说的“数据访问层”和“业务逻辑层”的界限是什么了。

  44. 老赵
    admin
    链接

    老赵 2009-10-19 14:35:00

    @小No
    你们是怎么做数据访问层测试的啊?

  45. 老赵
    admin
    链接

    老赵 2009-10-19 14:37:00

    @Colin Han
    所以其实我在项目中基本上通用一个模式,根据这个模式构建的数据访问层里其实就是NH的各种操作,至于其他的比如基本业务逻辑,安全性控制都不属于这些。

  46. Colin Han
    *.*.*.*
    链接

    Colin Han 2009-10-19 14:51:00

    小No:
    @Jeffrey Zhao
    如果仅仅是因为测试用例做得不够,就把本来应该在单元测试发现的问题,扔到运行时才发现,那这个单元测试是不是做得没什么大意义呢?

    当然我也赞同,做单元测试有时真的很难做到非常完整。你说对Binder只跑2,3个case,如果这两2,3个case能包含大部分的情况(80%以上),那么我认为也足够了。那么就用这个2,3个case先写测试代码。通过了,到时重构的时候还是会提取Tokenizer的,到时再对Tokenizer进行详细的测试也是可以的啊。



    可测试性驱动的开发可能确实违背了敏捷方法论中的“尽可能简单的完成需求”的路线。

    以我个人的经验,如果刚开始没有很好的规划,其实后期是很难重构出来一个好的设计的。这其中需要考虑人的惰性因素和项目工期的压力 :)

    刚开始对系统做必要的抽象还是对提高系统的可维护性和柔性很有帮助的。但是,这个抽象必须是把握住问题的本质。这一点,却往往是我们难以把握的。

    可测试性驱动的设计就是一种方式帮助我们避免抽象过头。

  47. 小No
    *.*.*.*
    链接

    小No 2009-10-19 14:55:00

    Jeffrey Zhao:
    @小No
    你们是怎么做数据访问层测试的啊?



    就直接连接一个单元测试数据库获取数据,然后再做测试。由于数据访问层的代码一般不会相互引用,所以对数据访问层的代码修改最多只会影响一个类的代码,因此每次只要执行相关类的测试方法就可以了,而一个类的测试方法不会太多,所以就算是连接上数据库,时间上还是可以接受的。

    实际做法是这样:
    1. 新建一个专门用于单元测试的数据库,数据结构跟实际业务数据库一样
    2. 初始化一些测试数据,初始化完成之后,这个数据库就不再做任何修改,除非数据结构有变化。
    3. 在测试涉及到CRUD操作的方法时,测试方法都要打上AutoRollbackAttribute,以保证方法结束后,数据库能还原到初始状态。
    4. 所有的断言都是拿单元测试数据库里现存的测试数据来做断言

  48. 麒麟.NET
    *.*.*.*
    链接

    麒麟.NET 2009-10-19 16:23:00

    如果一个私有方法需要测试,那么说明它的逻辑相对较为复杂,而且有独立的职责,应该将其提取到外部的类型中。

    事实上,逻辑较为复杂的私有方法很多,如Xxx方法会有与之对应的XxxInternal方法之类的,XxxInternal往往就会很复杂,这在.NET类库中也处处可见啊

  49. 老赵
    admin
    链接

    老赵 2009-10-19 18:29:00

    @麒麟.NET
    不过这需要测试,至少……改成internal,然后要测试……

  50. netwy
    *.*.*.*
    链接

    netwy 2009-10-19 20:39:00

    在数据库应用系统中适合用tdd吗?大概是什么流程?

  51. 老赵
    admin
    链接

    老赵 2009-10-19 21:45:00

    @netwy
    什么叫做数据库应用系统?

  52. DiamondTin[未注册用户]
    *.*.*.*
    链接

    DiamondTin[未注册用户] 2009-10-20 09:35:00

    老赵,我觉得你说的“可测试性驱动开发”和“测试驱动开发”的结果是非常接近的,可是实际观察下来我觉得不是一样的。我觉得测试驱动开发的一个好处就是从外界向内部逐渐迫近,逐渐的暴露的你意图。意图其实是更接近你的商业流程和商业价值的。所以更接近与“意图驱动开发”。因为TDD鼓励从系统的外部行为向内部行为逐步细化,逐步测试-通过,循环。这样产生的结果就是方法小,方法名考究,高层的代码非常容易读懂(经常达到看起来就像文档)。
    可是在“可测试性驱动开发”中,我觉得随时考虑的还是“可测试性”,但是“可测试性”本身与商业价值无关。所以驱动开发的东西就转变为程序员自己的一些“修养”问题了,你可能会倾向于使用模式来解决一些微观问题,让代码具有“可测试性”。这种做法可以产生组织良好的代码,它对于程序员来说易读(容易理解算法细节),但是这样的代码在被维护更久以后容易产生与商业价值的偏离。还有就是“测试性驱动”的经济问题,如果一个测试是纯技术意图驱动的,那么就是追本逐末了。
    测试驱动开发中的ATDD(验收测试驱动开发)鼓励你将系统的流程Spec在实现前写出来作为驱动,而BDD则从微观行为上将你的领域模型的行为Spec写出来驱动,这两种方式我想才是经典TDD的“新外衣”,因为他们都继承了“意图驱动”并进化为“商业意图(价值)驱动”。
    此文甚好,只是我觉得上面问题是个关键,”目标“最好还是在产生”策略“之前找到比较好,所以才写这个评论,冒犯了 ^___^

  53. 老赵
    admin
    链接

    老赵 2009-10-20 09:48:00

    @DiamondTin
    你说的让我有一定启发,似乎我的确是从技术角度进行的一些东西,目标有些“技术化”。
    而传统TDD是从外部行为进行,强调的是“外部表现正确”。
    更深一些的东西我还要想想,比如我的做法是否的确容易产生偏差,该怎么搞。
    // 没啥冒犯的,不要那么客气,呵呵。

  54. wsky
    *.*.*.*
    链接

    wsky 2009-10-20 10:44:00

    Mark

  55. netwy
    *.*.*.*
    链接

    netwy 2009-10-20 12:43:00

    我可能太关注实现的细节了,比如:入库单这样一个模块,后台有数据库资源,我该怎样做tdd哪?

  56. Colin Han
    *.*.*.*
    链接

    Colin Han 2009-10-22 21:41:00

    @小No
    今天突然想到,小No提出的对数据访问层单元测试的方案有问题。这个方案无法支持后续的重构工作。

    比如:随着业务理解的深入,我们认为一个属性应该是订单的,而不是产品的。这种时候,重构就会带来维护数据库的工作量。而且,往往这时候的工作量都不小呢。

    因此,我还是认为,数据访问层应该尽可能的薄。薄到不需要测试才是最理想的状态。并且,应该尽可能的晚构造数据库。我设想中,DB4O这种数据库也可能存在难以重构的问题。

    但是,会不会存在这样一种可能呢?维护数据库的工作量比Mock数据访问层的工作量更小。我觉的也许Mock数据访问层的时候也难免需要构造很多数据。将来真的需要重构的时候,这些数据的维护也会很费劲。

    我做数据库相关的开发很少,不知道老赵和小No怎么看?

  57. 老赵
    admin
    链接

    老赵 2009-10-23 12:03:00

    @Colin Han
    我用Migrator.NET,感觉还是相当方便的。

  58. 小No
    *.*.*.*
    链接

    小No 2009-10-23 12:39:00

    @Colin Han
    其实我也在想数据访问层的单元测试是不是必要的。但其实做了也是有一些好处的,我发现我们项目中很多人写代码是很粗心的,就连简单的CRUD也经常出错,做单元测试起码保证能正确获取到数据,不用到了集成测试时发现这么低级的错误。还有一些比较复杂的多表关联操作,我觉得做一些单元测试还是有必要的。

    但是你说的重构问题,既然数据库结构都改了,无论你是直接从数据库读取数据或者是模拟数据,都会面临同样的问题。

    或许老赵所说的Migrator.NET是一个比较省事的工具。

  59. S.Y.M
    120.42.54.*
    链接

    S.Y.M 2010-04-02 18:12:16

    曾经我也想尝试Test First,但真的有点难,除非逻辑比较明显。但感觉倒是可以把这些Test-First的代码在纸上画画,这样确实对设计有利,《.net framework design guidelines》也这么说,但是把它们写成真实的测试代码,也太累人了,关键的是,实现真实代码到一半时,往往都要改许许多的设计(虽然大局设计没变),这时那些测试代码就遭殃了。 看到老赵在这种情况下也不推崇TDD,我心里踏实了。

    另外,我碰到点问题,想请教下:

    比如我现在有一类CmsPage,表示Cms的一个页面,然后在db中它有一字段TemplateName,表示模板名称,在对象模型中,我有另外一个Template类(不可持久化),所以我希望CmsPage有个Template属性,它将直接返回Template类实例,这下,问题就来了,我的Template在外部另有一个类TemplateManager来管理,那从我的类的调用者角度来说,一旦得到了一个CmsPage实例,他只要调用CmsPage.Template就能得到Template实例,但如此一来,我的CmsPage.Template属性中就不得不使用类似这样的代码: Template template = TemplateManager.GetInstance().FindTemplate(TemplateName); 也就是说,CmsPage.Template属性在依赖于一个Singleton,假设TemplateManager.GetInstance()返回的TemplateManager是缓存于HttpContext的对象,如此一来,这个代码和web的耦合度就很高,我一直在这纠结着,干脆就这样子吧?因为系统99%是基于web,这个需求是比较稳定的,但也难免将来出现个windows client。

    那如果我让TemplateManager从外部进入,甚至抽取出ITemplateManager,又显得过于复杂,那就不能用CmsPage.Template属性,而可能要加个CmsPage.GetTemplate(ITemplateManager manager)方法,如果是其它的场景,这可能也ok,但对于我的CmsPage的客户端来说,我甚至不希望它知道有个ITemplateManager,客户端知道它在目前看来意义不大,而如果用GetTemplate(ITemplateManager manager),那调用者就得多学习一个接口。

    呃,这个纠结如何解啊? 3Q

  60. S.Y.M
    120.42.54.*
    链接

    S.Y.M 2010-04-02 18:14:09

    呃,其实我的问题也就在于,更“抽象”(同时也带来了更好的可测试性)的结果是设计变复杂,用一句万金油般的话说就是这要权衡,但究竟如何权衡呢? 3Q3Q

  61. 方法
    59.37.5.*
    链接

    方法 2012-04-24 17:33:27

    干脆去掉语言中的private ,你过渡设计过头了吧。

  62. 方法
    59.37.5.*
    链接

    方法 2012-04-24 17:37:08

    一个线性hash的设计。

    1. 除了满足基本增删改查,
    2. 该hash需要能扩展桶数(以2^n [n = 1,2,3])。

    这种功能也不属于对外的。是不是也要提取个抽象类出来阿。要不要测试阿?

  63. 老赵
    admin
    链接

    老赵 2012-04-25 10:17:56

    @方法

    private自然是要的,你怎么看出应该去掉的?如果对扩展桶数有明确要求,那肯定也是要测试的,是不是通过提取抽想类可以再议。

  64. 方法
    59.37.5.*
    链接

    方法 2012-04-25 10:49:17

    因为,我感觉的你的设计最后好像就是抽象出一堆只有公共接口的设计类(粗略的看了一下)。呵呵。TDD我估计100个团队能实践出来100种模式。作为一种理论,好像还缺少实践的指导。

    此外除了需求对测试用例(先实现的)的冲击外,我倒想设计的大变动对测试用例的冲击又多大。毕竟,世界不完美,需求不完美,设计不完美,实现不完美,测试先行而设计出来的Testcase(也不可能是完美的)的维护成本会不会成为另外一个噩梦呢?

  65. 方法
    59.37.5.*
    链接

    方法 2012-04-25 11:08:47

    参考这篇文章:http://www.infoq.com/cn/news/2009/02/unit-test-private-method

    其实觉得在TDD实践者对于私有方法的是否测试都没有统一认识(有得直接不测试,有得说可以通过public接口间接测试等等)。

    在比如对于开发c库,其隐藏在接口之下的函数难道都可以不用测试了?因为只针对外在行为测试,不对实现测试(或许我可以只实现一个接口,其他的都实现位私有的方法,呵呵,这种情况下,绿灯是有了,程序员也可以说,看我的代码都通过测试了。因为私有方法我不需要去测试)。 TDD对于业务类型的产品可能会好点。

    总而言之,TDD有优点,同时觉得它又会像是把开发人员当做流水线上的螺丝钉一样,而且它的有些理念过于极端了。比如:

    TDD的三条军规 软件测试 

    这些年来,我喜欢用下面这三条简单的规则来描述测试驱动开发:

    • 除非这能让失败的单元测试通过,否则不允许去编写任何的产品代码。
    • 只允许编写刚好能够导致失败的单元测试。 (编译失败也属于一种失败)
    • 只允许编写刚好能够导致一个失败的单元测试通过的产品代码。

    个人觉得,TDD的测试用例也应该借助迭代这个工具,积极拥抱变化。

  66. 方法
    59.37.5.*
    链接

    方法 2012-04-25 11:49:04

    一个线性hash的设计。

    1. 除了满足基本增删改查,
    2. 该hash需要能扩展桶数(以2^n [n = 1,2,3])。

    这种功能也不属于对外的。是不是也要提取个抽象类出来阿。要不要测试阿?

    假设一开始有两个桶(2^1)(假设每个桶最大能有2个元素,举例子) 因此 序列:0,1,2,3将会把0,2散列到第一个桶, 1,3散列到第二个桶中。如果动态扩展桶位4个桶(2^2),则理论上会有一半的元素被散列到扩展的桶内。

    所以这其实是你选择了线性hash这种设计附带而来的特性,如果你一开始的设计选择其他数据结构可能就没这个问题。你可以理解为吃肉就得冒变胖的危险,吃素就得冒营养不良的危险。

    我其实想表达的意思是:这种内部的行为,并不见得可以通过将它抽象到另外一个逻辑类中来实现,如果一定这么做,我反而觉得破坏了封装,我也不觉得有什么合理的抽象可以独立描述它。它就是这个hash内部的,而且不能暴露给外部的。

    所以才有疑问?是否要测试?怎么测试?通过公共接口来测试不失为一种方法。此外,在kent beck的《测试驱动开发》举了一个菲波纳切数列的例子,就这么个简单破例子,它搞了好几个测试用例。比这个破例子复杂的没暴露的函数 方法多了去了吧(参见操作系统,各种库等等)。一句不是外部的行为,就免于测试了?如果测试了就代表代码写的太烂。不知道会伤害多少人阿。

    TDD提出来这么久了,说实话关于它的经典书籍真的还是不多阿(那本100多页的书说实话就是个试验品,对于实践我没感觉),发展遇到什么瓶颈了?

  67. 方法
    59.37.5.*
    链接

    方法 2012-04-25 11:51:03

    共产主义很美,现实是我们处在社会主义,还是初级阶段,还要100年不变。

  68. 方法
    59.37.5.*
    链接

    方法 2012-04-25 15:07:18

    想起一个笑话: 一人给猴子喂花生,猴子总是往屁 yan塞一下然后再吃.那人不解问饲养员,答:它去年吃了个大桃,费了好大劲才把桃核拉出来.所以现在它吃什么都得先用屁yan量一下

    如果你喂给这只猴子一只西瓜咋办? 如果桃子是无核的呢?

  69. 方法
    59.37.5.*
    链接

    方法 2012-04-25 15:17:10

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我