细数35个单元测试准则 “Hello, world!”

1.保持单元测试小巧、快速

理论上,任何代码提交前都应该完整跑一遍全部测试套件。保持测试代码执行符合预期,这样可以缩短迭代开发周期。程序员

2.单元测试应该是全自动/非交互式的

测试套件一般是按期执行的,执行过程必须彻底自动化才有意义。输出结果须要人工检查的测试不是一个好的单元测试。面试

3.让单元测试很容易跑起来

对开发环境进行配置,最好是敲一条命令或是点击一个按钮就能把单个测试用例或测试套件跑起来。数据库

4.对测试进行评估

对执行的测试进行覆盖率分析,获得精确的代码执行覆盖率,并调查哪些代码未被执行。app

5.当即修正失败的测试

每一个开发人员在提交前都应该保证新的测试用例执行成功,当有代码提交时,现有测试用例也都能跑通。
若是一个按期执行的测试用例执行失败,整个团队应该放下手上的工做先解决这个问题。框架

6.把测试维持在单元级别

单元测试即类(Class)的测试。一个“测试类”应该只对应于一个“被测类”,而且“被测类”的行为应该被隔离测试。必须谨慎避免使用单元测试框架来测试整个程序的工做流,这样的测试即低效又难维护。工做流测试(译注:指跨模块、类的数据流测试)有它本身的地盘,但它毫不是单元测试,必须单独创建和执行。dom

7.由简入繁

再简单的测试也远远赛过彻底没有测试。一个简单的“测试类”会促使创建“被测类”基本的测试骨架,能够对构建环境、单元测试环境、执行环境以及覆盖率分析工具等有效性进行检查,同时也能够证实“被测类”可以被整合和调用。
下面即是单元测试版的“Hello, world!”:函数

void testDefaultConstruction()
{
Foo foo = new Foo();
assertNotNull(foo);
}

8.保持测试的独立性

为了保证测试稳定可靠且便于维护,测试用例之间决不能有相互依赖,也不能依赖执行的前后次序。(实际上TestNG有提供依赖的功能,或许某些场景也须要依赖。)工具

9.Keep tests close to the class being tested

[译注:有意翻译该规则,我的认为本条规则值得商榷,大部分C++、Objective-C和Python库均把测试代码从功能代码目录中独立出来,一般是建立一个和src目录同级的tests目录,被测模块、类名以前也经常不加Test 前缀。这么作保证功能代码和测试代码隔离、目录结构清晰,而且发布源码的时候更容易排除测试用例单元测试

]
If the class to test is Foo the test class should be called FooTest (not TestFoo) and kept in the same package (directory) as Foo. Keeping test classes in separate directory trees makes them harder to access and maintain.
Make sure the build environment is configured so that the test classes doesn’t make its way into production libraries or executables.

10.合理的命名测试用例

确保每一个方法只测试“被测类”的一个明确特性,并相应的命名测试方法。典型的命名俗定是“test[MethodName]”,好比“testSaveAs()”、“testAddListener()”、“testDeleteProperty()”等。测试

11.只测公有接口

单元测试能够被定义为经过类的公有API对类进行测试。一些测试工具容许测试一个类的私有成员,但这种作法应该避免,它让测试变得繁琐并且更难维护。若是有私有成员确实须要进行直接测试,能够考虑把它重构到工具类的公有方法中。但要注意这么作是为了改善设计,而不是帮助测试。

12.当作是黑盒

站在第三方使用者的角度,测试一个类是否知足规定的需求。并设法让它出问题。

13.当作是白盒

毕竟被测试类是程序员自写自测的,应该在最复杂的逻辑部分多花些精力测试。

14.芝麻函数也要测试

一般建议全部重要的函数都应该被测试到,一些芝麻方法好比简单的setter和getter均可以忽略。可是仍然有充分的理由支持测试芝麻函数:
“芝麻”很难定义,对于不一样的人有不一样的理解。
从黑盒测试的观点看,是没法知道哪些代码是芝麻级别的。
即使是再芝麻的函数,也可能包含错误,一般是“复制粘贴”代码的后果:

private double weight_;
private double x_, y_;
public void setWeight(int weight)
{
weight = weight_;  // error
}
public double getX()
{
return x_;
}
public double getY()
{
return x_;  // error
}

所以建议测试全部方法,毕竟芝麻用例也容易测试。

15.先关注执行覆盖率

区别对待“执行覆盖率”和“实际测试覆盖率”。测试的最初目标应该是确保较高的执行覆盖率,这样能保证代码在 少许参数值输入时能执行成功。一旦执行覆盖率就绪,就应该开始改进测试覆盖率了。注意,实际的测试覆盖率很难衡量(并且每每趋近于0%)。
思考如下公有方法:

void setLength(double length);
调用“setLength(1.0)”你可能会获得100%的执行覆盖率。但要达到100%的实际测试覆盖率,有多少个 double浮点数这个方法就必须被调用多少次,而且要一一验证行为的正确性。这无疑是不可能的任务。

16.覆盖边界值

确保参数边界值均被覆盖。对于数字,测试负数、0、正数、最小值、最大值、NaN(非数字)、无穷大等;对于字符串,测试空字符串、单字符、非ASCII字符串、多字节字符串等;对于集合类型,测试空、一、第一个、最后一个等;对于日期,测试1月1号、2月29号、12月31号等。被测试的类自己也会暗示一些特定状况下的边界值。 要点是尽量完全的测试这些边界值,由于它们都是主要“疑犯”。

17.提供一个随机值生成器

当边界值都覆盖了,另外一个能进一步改善测试覆盖率的简单方法就是生成随机参数,这样每次执行测试都会有不一样的输入。
想要作到这点,须要提供一个用来生成基本类型(如:浮点数、整型、字符串、日期等)随机值的工具类。生成器应该覆盖各类类型的全部取值范围。
若是测试时间比较短,能够考虑再裹上一层循环,覆盖尽量多的输入组合。下面的例子是验证两次转换“little endian”和“big endian”字节序后是否返回原值。因为测试过程很快,可让它跑上个一百万次。

 

void testByteSwapper()
{
for (int i = 0; i < 1000000; i++) {
    double v0 = Random.getDouble();
    double v1 = ByteSwapper.swap(v0);
    double v2 = ByteSwapper.swap(v1);
    assertEquals(v0, v2);
}
}

18.每一个特性只测一次

在测试模式下,有时会不由自主的滥用断言。这种作法会致使维护更困难,须要极力避免。仅对测试方法名指示的特性进行明确测试。
由于对于通常性代码而言,保证测试代码尽量少是一个重要目标。

19.使用显式断言

应该老是优先使用“assertEquals(a, b)”而不是“assertTrue(a == b)”,由于前者会给出更有意义的测试失败信息。在事先不肯定输入值的状况下,这条规则尤其重要,好比以前使用随机参数值组合的例子。

20.提供反向测试

反向测试是指刻意编写问题代码,来验证鲁棒性和可否正确的处理错误。
假设以下方法的参数若是传进去的是负数,会立马抛出异常:
void setLength(double length) throws IllegalArgumentException
能够用下面的方法来测试这个特例是否被正确处理:

 

try {
setLength(-1.0);
fail();  // If we get here, something went wrong
}
catch (IllegalArgumentException exception) {
// If we get here, all is fine
}

21.代码设计时谨记

编写和维护单元测试的代价是很高的,减小代码中的公有接口和循环复杂度是下降成本和使高覆盖率测试代码更易于编写和维护的有效方法。
一些建议:

22.使类成员常量化,在构造函数中进行初始化。减小setter方法的数量。

23.限制过分使用继承和公有虚函数。

24.经过使用友元类(C++)或包做用域(Java)来减小公有接口。

25.避免没必要要的逻辑分支。

26.在逻辑分支中编写尽量少的代码。

27.在公有和私有接口中尽可能多用异常和断言验证参数参数的有效性。

28.限制使用快捷函数。

对于黑箱而言,全部方法都必须一视同仁的进行测试。考虑如下简短的例子:

 

public void scale(double x0, double y0, double scaleFactor)
{
// scaling logic
}
public void scale(double x0, double y0)
{
scale(x0, y0, 1.0);
}

29.删除后者能够简化测试,但用户代码的工做量也将略微增长。

30.不要访问预设的外部资源

单元测试代码不该该假定外部的执行环境,以便在任什么时候候和任何地方都能执行。为了向测试提供必需的资源,这些资源应该由测试自己提供。
好比:一个解析某类型文件的类,能够把文件内容嵌入到测试代码里。在测试的时候写入到临时文件,测试结束再删除,而不是从预约的地址直接读取。

31.权衡测试成本

不写单元测试的代价很高,可是写单元测试的代价一样很高。要在这二者之间作适当的权衡,若是用执行覆盖率来衡量,业界标准一般在80%左右。
很典型的,读写外部资源的错误处理和异常处理就很难达到百分百的执行覆盖率。模拟数据库在事务处理到一半时发生故障并非办不到,但相对于进行大范围的代码审查,代价可能太大了。

32.安排测试优先次序

单元测试是典型的自底向上过程,若是没有足够的资源测试一个系统的全部模块,就应该先把重点放在较底层的模块。

33.测试代码要考虑错误处理

考虑下面的这个例子:

Handle handle = manager.getHandle();
assertNotNull(handle);
String handleName = handle.getName();
assertEquals(handleName, "handle-01");

若是第一个断言失败,后续语句会致使代码崩溃,剩下的测试都没法执行。任什么时候候都要为测试失败作好准备,避免单个失败的测试项中断整个测试套件的执行。上面的例子能够重写成:

Handle handle = manager.getHandle();
assertNotNull(handle);
if (handle == null) return;
String handleName = handle.getName();
assertEquals(handleName, "handle-01");

34.写测试用例重现Bug

每上报一个Bug,都要写一个测试用例来重现这个Bug(即没法经过测试),并用它做为成功修正代码的检验标准。

35.了解局限

单元测试永远没法证实代码的正确性!!
一个跑失败的测试可能代表代码有错误,但一个跑成功的测试什么也证实不了。
单元测试最有效的使用场合是在一个较低的层级验证并文档化需求,以及回归测试:开发或重构代码,不会破坏已有功能的正确性。


以上内容就是本篇的所有内容以上内容但愿对你有帮助,有被帮助到的朋友欢迎点赞,评论。

若是对软件测试、接口测试、自动化测试、面试经验交流。感兴趣能够关注我,咱们会有同行一块儿技术交流哦。