单元测试规范

一.可衡量:单测的编写应该是可以用具体的指标衡量的

1、单测通过率要求100%,行覆盖率要求50%。

解释:通过率100%没啥好多说的,如果单测跑不通过,那不是单测有问题就是代码逻辑有问题。覆盖率的话可以根据具体的工程进行微调,建议不应小于40%,越底层的代码覆盖率应该越高,越新的代码覆盖率也应该越高。

2、老代码有逻辑变更时,单测也应该做相应的变更。

解释:这点的目的也是为了保证单测通过率100%。同时,这部分功能应该也属于改次功能的测试回归范围内。

3、新业务提测前,必须保证老单测的通过率也保持100%。

解释:这点的目的是为了防止回溯问题的出现。

二.独立性:单测应该是独立且相互隔离的

4、一个单测只测试一个方法。

解释:保证了单测的独立性。当单测出错的时候也能够明确知道是哪个方法出了问题。但这并不是说一个方法只对应一个单测,因为为了覆盖方法内的不同分支,我们可以为一个方法创建多个单测。

5、单测不应该依赖于别的单测。

解释:保证了单测的独立性。每个单测应该都能独立运行。不应该有A单测跑完才能跑B单测的情况。

6、单测如果涉及到数据变更,必须进行回滚。

解释:保证了单测的隔离性。如果单测运行后在数据库中产生了数据,那这些脏数据可能干扰测试同学的测试工作,且也可能影响别的单测的运行结果。

7、单测应该测试目标方法本身的逻辑,对于被测试的方法内部调用的非私有方法应进行mock,推荐使用Mockito进行mock。

解释:目标方法存在内部调用情况,进行mock可以屏蔽其他方法对目标方法的影响。这样保证了单测的独立性,一个单测只保证它测试的目标方法的逻辑正确性,而不应该受其内部调用方法的逻辑的影响,这部分应该是这些内部调用的方法对应的单测的责任。但是真实情况中,这一点是最难被严格执行,因为这样做就意味着需要对所有的方法都设计单测,比如a调用b调用c的情况,需要至少设计三个单测,而不能只对a设计单测来覆盖整个调用链。不过,这不正是单测的含义吗?对最小的逻辑单元——方法进行测试,如果对于一个调用链进行测试,更像是集成测试的范畴了。而且如果不这么做,我们就会违反上面的第4条“一个单测只测试一个方法”。只有一种情况例外,方法内部调用的是私有方法,这样的话是可以通过调用方的单测一并测试的,见下面的第13条“私有方法通过调用类的单测进行测试”。我们可以试想一种情况,当一个项目由很多人协同开发时,我怎么才能放心使用另一个人开发的方法?至少得提供单测吧,如果这个方法的测试是在其调用方的单测中的,那就没有直接对应的单测了,这样也就无法保证该方法是否被妥当测试过了。

三.规范性:单测的编写需要符合一定规范

8、对实现类进行测试而非接口。

解释:面向接口编程,面向实现测试。

9、单测应该是无状态的。

解释:即单测应该可以重复执行,且无论跑几次都应该保证通过率。比如有些方法会对当前时间进行判断,对于这类方法的单测也需根据当前时间的不同而进行不同的测试。

10、覆盖范围应包括所有提供了逻辑的类:service层、manager层、自定义mapper等,甚至还有部分提供业务逻辑的controller层代码。

解释:只要是提供了逻辑的就应该测试,不过个人并不建议在controller层提供业务逻辑,具体原因参考《设计之道-controller层的设计》

11、覆盖范围不应包括自动生成的类:如MyBatis Generator生成的Mapper类、Example类,不应包括各种POJO(DO,BO,DTO,VO...),也不应包括无业务逻辑的controller类。

解释:自动生成的类有啥好测的?POJO的getter/setter有啥好测的?没有提供业务逻辑的controller类有啥好测的?这些被排除的类应该在覆盖率统计中被剔除。

12、私有方法通过调用类的单测进行测试。

解释:因为私有方法在测试类内没法直接调用,除非使用反射。

13、单测要覆盖到正常分支和异常分支,使用专门的异常测试属性junit(expected)/testng(expectedExceptions)。禁止使用try-catch。

解释:很多同学的单测覆盖率不达标,就是因为只覆盖了正常的分支而遗漏的异常的分支。异常的测试和正常的一样重要,也就是该报错的时候就应该报错。有些同学为了达到单测的覆盖率和通过率的指标,在单测中使用try-catch,这也是不允许的,应该使用专门的异常测试注解。

14、如果被测试的方法的逻辑体现在方法返回或成员变量中,则使用Assert断言验证该返回或成员变量。

解释:如果一个方法的内部组装了一个返回值,或变更了一个成员变量,那么应该使用Assert来验证该返回值或成员变量是否符合预期。

比如下面的三个方法,前两个的逻辑都是体现在返回值上,后一个的逻辑体现在成员变量中。

    /**
     * 逻辑体现在返回值
     *
     * @return
     */
    public String displayName() {
        String name = "HangzhouZoo";
        return "Zhejiang " + name;
    }

    /**
     * 逻辑体现在返回值
     *
     * @return
     */
    public String luxuryShow() {
        String show = dog.run();
        return "luxury!! " + show;
    }

    /**
     * 逻辑体现在成员变量
     */
    public void close() {
        this.open = false;
    }

那么我们就可以使用Assert断言来测试这些逻辑:

    //逻辑在方法返回体现
    @Test
    public void displayName() {
        Assert.assertEquals("Zhejiang HangzhouZoo", hangzhouZoo.displayName());
    }

    //逻辑在方法返回体现
    @Test
    public void luxuryShow() {
        when(dog.run()).thenReturn("dog show");
        Assert.assertEquals("luxury!! dog show", hangzhouZoo.luxuryShow());
    }

    //逻辑在成员变量中体现
    @Test
    public void close() {
        Assert.assertTrue(hangzhouZoo.isOpen());
        hangzhouZoo.close();
        Assert.assertFalse(hangzhouZoo.isOpen());
    }

16、如果被测试的方法的逻辑体现在内部的方法调用行为本身,则使用Mockito的verify验证内部方法调用的情况。

解释:有些方法的内部根据不同的条件会调用不同的方法,则应该验证该方法的调用是否符合预期。Mockito的verify可以验证被mock的方法是否调用了,甚至可以验证方法调用的次数。

比如下面这个方法有三分条件分支,分支一抛出异常,分支二调用内部方法,分支三组装返回值。

    /**
     * 逻辑体现在异常、方法调用行为和返回值
     *
     */
    @Override
    public String show(Animal animal) throws ZooException {
        if (animal instanceof Tiger) {
            throw new ZooException("tiger is not allowed");
        } else if (animal instanceof Dog) {
            return animal.run();
        } else {
            return "only dogs here";
        }
    }

其中分支二的逻辑就体现在方法调用的行为上,我们可以通过verify来验证方法是否如预期一样调用,也可使用times验证方法调用的次数。

    //被测试的方法的逻辑体现在内部方法的调用行为本身
    @Test
    public void show() throws Exception {
        when(dog.run()).thenReturn("dog run");
        hangzhouZoo.show(dog);
        //验证方法被调用过了
        verify(dog).run();
        //也可以通过times参数来验证方法具体被调用的次数
        verify(dog, times(1)).run();
        //验证另一个分支,逻辑体现在返回值
        Assert.assertEquals("only dogs here", hangzhouZoo.show(new Cat()));
    }

当然,还记得第13条“异常分支也需要测试么”,我们还需要写一个单测来覆盖异常分支:

    //测试异常分支
    @Test(expected = ZooException.class)
    public void showForEx() throws Exception {
        hangzhouZoo.show(new Tiger());
    }
  1. 如果被测试的方法的逻辑体现在内部方法调用的参数中,即方法的逻辑用于构建内部调用方法的参数,则使用Mockito的verify验证内部方法调用的参数。

解释:有些方法的内部会组装一个对象,然后将这个对象作为参数传入另一个内部方法。使用Mockito的verify可以验证被mock的方法被调用的参数。如果是简单类型,可以直接验证,如果是复杂类,则需要扩展ArgumentMatcher类来做验证。

下面这个方法的逻辑体现在内部调用方法的参数构造上:

    /**
     * 逻辑体现在参数构造-基本类
     *
     * @param times
     */
    public void bark(int times) {
        int actualTimes = times * 10;
        dog.bark(actualTimes);
    }

由于参数类型是基本类,所以我们可以直接用verify来验证:

    //逻辑在参数体现-简单类型
    @Test
    public void bark() {
        doNothing().when(dog).bark(anyInt());
        hangzhouZoo.bark(3);
        verify(dog).bark(30);
        //与上面等价
        verify(dog).bark(eq(30));
    }

不过如果像下面这样的参数是复杂类的,就需要扩展一下:

     /**
     * 逻辑体现在参数构造-复杂类
     *
     * @param
     * @return
     */
    public String feedVegetable() {
        Food tomato = Food.builder().name("tomato").build();
        return dog.eat(tomato);
    }

自定义参数匹配器:

/**
 * @Author: Sawyer
 * @Description: 自定义参数匹配规则
 * @Date: Created in 2:02 PM 2019/10/15
 */

public class ObjectMatcher<T> extends ArgumentMatcher<T> {

    private Object expected;
    private Function<T, Object> getProperty;

    public ObjectMatcher(Object expected, Function<T, Object> getProperty) {
        this.expected = expected;
        this.getProperty = getProperty;
    }

    @SuppressWarnings("unchecked")
    @Override
    public boolean matches(Object actual) {
        return getProperty.apply((T) actual).equals(expected);
    }
}

测试的时候使用argThat校验方法参数:

    //逻辑在参数体现-复杂类
    @Test
    public void feedVegetable() {
        when(dog.eat(any())).thenReturn("dog eat");
        hangzhouZoo.feedVegetable();
        //验证参数
        verify(dog).eat(argThat(new ObjectMatcher<>("tomato", Food::getName)));
    }

17、单测应在相应的目标方法开发完后立即编写,如能在开发前就开始编写则更好(TDD)。

解释:这点可能会违背很多开发同学的认知,怎么可能先写单测再写代码呢?实际上,如果稍微了解下测试驱动开发(Test-Driven Development),就会发现这并非异想天开,反倒是顺理成章的事。我认为有两种场景下单测的习惯是很容易能够推动的,第一种是团队里没有测试人员,代码质量完全由开放人员把控;而第二种就是软件开发流程使用的是TDD的方式,这样天然的就保证了单测必须存在。

原文:设计之道-单元测试规范 - 简书 (jianshu.com)