单元测试规范
一.可衡量:单测的编写应该是可以用具体的指标衡量的
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());
}
- 如果被测试的方法的逻辑体现在内部方法调用的参数中,即方法的逻辑用于构建内部调用方法的参数,则使用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的方式,这样天然的就保证了单测必须存在。