TDD私有方法测还是不测-2

单元测试是否测试私有方法

在TDD开发中,私有的方法怎么办?需要测试吗?如果测怎么处理?不测又怎么办?来讨论一下这个问题。

先看一下范围。
如果反过来问,可以澄清楚一下这个问题的范围。如果私有方法是,简单的“小函数”, 比如为表达代码意图而提取的小函数,那么显然不需要测。 如果私有方法是,复杂的大函数上百行, 那肯定会提取一个工具类,然后再去测试。当然这种情况同时需要考虑 类的职责是不是单一。 如果对这个问题还在有犹豫,那么暗示这个函数没有像“小函数”那么简单,也没有像“复杂函数“那么控制不了,复杂度在二者之间。对于这样的中等复杂度的私有函数如何测试

再看看,如果要测试私有函数,我们应该有哪些方法?

  • 私有方法直接变成公有方法。
    这个是最常用的方法,但是副作用用比较大。
    - 使的类的接口抽象层次不一致。是的代码和测试代码可读性不好。
    - 将里面的实现暴露出去,为后面代码重构和维护带来副作用。
    - 测试代码由于 要测试意图,又测实现,使的意图与实现混合,代码不清晰。

  • 通过一些奇技淫巧方法来调用私有方法。
    比如通过反射方式调用,语言的特性(C# particial, C++ 条件宏)。 这些的确都可以调用私有方法,但是会带来测试代码的复杂度和维护的难度。

这些方法带来的好处与坏处来比,性价比不高,不推荐。 下面看一种如何通过公有接口来测试私有方法的方式。

再看之前Diamond的那个例子, 当前的已经写2个测试用例:

@Test
public void test_characte_A() {

String[] expected = new String[] { "A" };
selfAssert(expected, Diamond.answer('A'));
}

@Test
public void test_characte_B() {

    String[] expected = {
        " A ",
        "B B",
        " A ",
    };        

selfAssert(expected, Diamond.answer('B'));
}

实现代码


 public static String[] answer(char c) {
  if( c == 'B') {
             return new String[] {
                " A ",
                "B B",
                " A "
           };
 
       }
       return  new String[] { String.valueOf(c) };
 }

这时候再加第三个case, 测试字母C:

@Test
    public void test_characte_C() {
    
        String[] expected = {
            "  A  ",
            " B B ",
            "C   C",
            " B B ",
            "  A  "
        };        
  selfAssert(expected, Diamond.answer('C'));
    }

对应硬编码的实现代码:

     public static String[] answer(char c) {

       if( c == 'C'){       
         return new String[]{
            "  A  ",
            " B B ",
            "C   C",
            " B B ",
            "  A  "
        };        
       }
       
       if( c == 'B') {
             return new String[] {
                " A ",
                "B B",
                " A "
           }; 
       }
       return  new String[] { String.valueOf(c) };
    }

现在发现规律,这个Diamond 是上下对称的,然后进行“merge”,那么我们可以对代码变形。 下面为减少代码,只将 if 里面变化代码列出。

 public static String[] answer(char c) {

   if( c == 'C'){       
         String[] top =  new String[]{
            "  A  ",
            " B B ",
            "C   C"
        };
        
        String[] down =  new String[]{
            "C   C",
            " B B ",
            "  A  "
        };

        return merge( top, down);       
  }
   //....
 }

下面是私有方法merge的实现:

  private static String[] merge(String[] top, String[] down){
    
        String[] result =  new String[5];
        System.arraycopy( top, 0, result, 0, top.length );
        System.arraycopy( down, 1, result, top.length, down.length-1 );
    
        return result;
    }

这个时候测试代码没有变化,还是3个测试用例; 但是代码经过重构变化,提取merge私有方法。这个私有方法是通过公共接口测试了。

下面可以继续对代码进行重构,去除重复。 Diamond 是上下对称的,那么有了上部分(top), 下部分(down)可以根据对称计算出来。继续变形。


 public static String[] answer(char c) {
    if( c == 'C'){       
        private static String[] merge(String[] top, String[] down){
        String[] top =  new String[]{
            "  A  ",
            " B B ",
            "C   C"
        };      

        String[] down =  getSymmetry(top);
        return merge( top, down) ;
       }
     // ...
}

下面是私有方法downSymmetry的实现:

    private  String[] getSymmetry(String[] origin){    
         String[] reverse = new String[origin.length];
         for(int i = 0; i< origin.length; i++){
            reverse[i] = origin[origin.length - i - 1];
         }        
         return reverse;
    }

这个时候测试代码没有变化,还是3个测试用力; 但是代码经过重构变化,提取downSymmetry私有方法。这个私有方法是通过公共接口测试了。

下面还可以接着去重构每一个行去寻找规律,继续重构。

    public static String[] answer(char c) {
       
       if( c == 'C'){
        String[] top =  new String[]{
            space(2) + "A" +space(2),
            space(1) + "B" + space(1) + "B" + space(1),
            space(0) + "C" + space(2) + "C" + space(0)
            space(0) + "C" + space(3) + "C" + space(0)
        };        
        String[] down =  getSymmetry(top); 
        return  merge( top, down) ;       
       }
    // ...
}

提取私有函数space(), 同样也在测试不变的情况下,通过公共接口测试该私有方法space。
发现每一行是有规律的,继续将重构,提取一个getLine的方法。


   public static String[] answer(char c) {
       
       if( c == 'C'){
         int width = 3;
        String[] top =  new String[]{
            space(2) + "A" +space(2),
            getLine(2),
            space(0) + "C" + space(3) + "C" + space(0)
        };        
        String[] down =  getSymmetry(top); 
        return  merge( top, down) ;       
       }
    // ...
 }
 private static String getLine(int lineNumber){
        return space(1) + "B" + space(1) + "B" + space(1);
    }

继续重构,一直到所有的行都被替换,真正的getLine 方法就提取成功了。 中间的步骤暂且就略过,感兴趣的可以代码演化同样测试用例没有修改,但是私有方法 getLine 通过公有方法被测试了。


   public static String[] answer(char c) {
       
    if( c == 'C'){

        int width = 5;
        int height = 3;

        String[] top =  new String[]{         
            getLine(1, width, height),
            getLine(2, width, height),
            getLine(3, width, height)           
        };
        
        String[] down =  getSymmetry(top); 
        return  merge( top, down) ;       
       }
    // ...
 }
   private static String getLine(int lineNum, int width, int height){
        String border = space(height - lineNum);
        if( lineNum == 1)
           return border + "A" + border;
        
        String lineChar = new Character( (char)('A' + lineNum - 1)).toString();
        String middleSpace = space(width - 2 - 2*(height - lineNum) );
        return  border + lineChar +
                middleSpace + 
                lineChar +border;
    }

在这小步迭代中提取getLine 方法,这个方法还是稍微复杂的。 但是我们每一次都有测试用例构建的安全网,在这个重构的过程中还是比较安全的。尽管没有显示的单独测试getLine 这个方法,但是一直有测试用例通过公有的接口覆盖其逻辑。编码过程中还是比较有信心的。

后面重构还继续,明显的引入循环,getLine 方法的后面两个参数都可以去除掉等等,继续重构下去,一直到自己感觉良好没有坏味道。 最终的代码在这个地方.

现在复盘,私有方法是如何被通过公有方法来测试的。 可以简单概况为先定义测试用例,然后基于重构一步一步的提取方法。在这个过程中,提取的私有方法,是小步迭代的,每一步都有测试验证的,所以也是安全放心的。 再看一下测试用例, 一开始就有3个测试用例一直没有变化,但是一直持续提供及时的反馈。

如果反过来, 同样的一个思路,先计算上面一部分(top),然后根据先上下对称得到下面部分(down),最后再合并。分别对应三个方法, getLine,getSymmetry,和 merge。 如果同样的思路从下往上去实现(先实现底层方法,然后再集成),这个时候这些私有方法是没办法直接通过公共接口来测试的。 这个又陷入刚开始的问题,私有方法怎么测试?

所以说基于公开接口来测试私有方法,是从上到下,小步迭代基于重构来实现的

总结

对于私有方法如何测试,思路是多种多样, 下面比较这几种对私有方法的测试方法的优劣。

类型 可读性 可维护性 代码量 测试的定位准确性
将私有方法改为公有 x x v v
奇技淫巧 xx xx - -
提取工具类 v v x v
公有方法来 v v - x

对于复杂私有方法如果太过于复杂,需要考虑是不是职责清晰,需要提取一个工具类来做测试。
对于类实现特有的一些私有方法,可以考虑考虑通过公有方法,从上到下基于重构来测试。但是出错,定位没有基于提取工具类的方法的可读性好。这个折中需要根据情况来权衡。

其他两种,会带来代码的可读性和维护行的额外复杂度,不建议。

NOTE:

对于这个题目还有一种叫做 [“Test recycle“的方法](http://claysnow.co.uk/recycling-tests-in-tdd/),也就是持续修改已有的测试用例,使的测试代码和产品代码共同演进的方法。当然也可以用来测试私有方法,有时候还是挺有诱惑力,但是不推荐。修改已有的测试用例,使的测试代码作为最终的安全网没有了。即使最终有了漂亮的实现,中间的过程的测试用例却没有留下来。
如果测试用例看做产品代码的一部分,作为财富一部分,那么这种做法相当于财富被挥霍掉。TDD开发过程中的测试用例是逐步增加的,对应的功能也是逐步增强,也就是一个增量开发的过程, 如同软件逐步长大的一个成长过程。而“test recycle“ 是一个测试用例,持续来刺激代码的演化,如果代码出现regression 很难出提供及时反馈。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容