美文网首页TDD(测试驱动开发)
TDD私有方法测还是不测-2

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

作者: 生活如同马拉松_yaguang | 来源:发表于2018-05-18 21:00 被阅读1次
    单元测试是否测试私有方法

    在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 很难出提供及时反馈。

    相关文章

      网友评论

        本文标题:TDD私有方法测还是不测-2

        本文链接:https://www.haomeiwen.com/subject/nxlgdftx.html