您现在的位置: 365建站网 > 365学习 > 单元测试规范_单元测试有哪些内容_如何进行单元测试

单元测试规范_单元测试有哪些内容_如何进行单元测试

文章来源:365jz.com     点击数:128    更新时间:2018-07-23 09:48   参与评论

为何需要个编写准则?

单元测试比实际实现可能还要难一些,它强迫你考虑清楚一些事情。

但单元测试本身应该简单、直接、易用和易于维护。

还要知道何时停止写测试并且开始写实现。

使用这个原则能够确保有效测试且达到目标,帮助避免一些明显的错误。

记住,编写糟糕的测试是在浪费时间,并会在以后造成更大的问题。

实施单元测试的时候, 如果没有一份经过实践证明的详细规范, 很难掌握测试的 “度”, 范围太小施展不开, 太大又侵犯 “别人的” 地盘. 上帝的归上帝, 凯撒的归凯撒, 给单元测试念念紧箍咒不见得是件坏事, 反而更有利于发挥单元测试的威力, 为代码重构和提高代码质量提供动力.

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

理论上, 任何代码提交前都应该完整跑一遍所有测试套件. 保持测试代码执行快能够缩短迭代开发周期.

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

测试套件通常是定期执行的, 执行过程必须完全自动化才有意义. 输出结果需要人工检查的测试不是一个好的单元测试.

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

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

4. 对测试进行评估

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

5. 立即修正失败的测试

每个开发人员在提交前都应该保证新的测试用例执行成功, 当有代码提交时, 现有测试用例也都能跑通.

如果一个定期执行的测试用例执行失败, 整个团队应该放下手上的工作先解决这个问题.

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

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

7. 由简入繁

再简单的测试也远远胜过完全没有测试. 一个简单的 “测试类” 会促使建立 “被测类” 基本的测试骨架, 可以对构建环境, 单元测试环境, 执行环境以及覆盖率分析工具等有效性进行检查, 同时也可以证明 “被测类” 能够被整合和调用.

下面便是单元测试版的 Hello, world! :

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

8. 保持测试的独立性

为了保证测试稳定可靠且便于维护, 测试用例之间决不能有相互依赖, 也不能依赖执行的先后次序.

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[what], 比如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月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. 代码设计时谨记测试

编写和维护单元测试的代价是很高的, 减少代码中的公有接口和循环复杂度是降低成本, 使高覆盖率测试代码更易于编写和维护的有效方法.

一些建议:

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

限制过度使用继承和公有虚函数.

通过使用友元类 (C++) 或包作用域 (Java) 来减少公有接口.

避免不必要的逻辑分支.

在逻辑分支中编写尽可能少的代码.

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

限制使用快捷函数. 对于黑箱而言, 所有方法都必须一视同仁的进行测试. 考虑以下简短的例子:

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

删除后者可以简化测试, 但用户代码的工作量也将略微增加.

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

单元测试代码不应该假定外部的执行环境, 以便在任何时候/任何地方都能执行. 为了向测试提供必需的资源, 这些资源应该由测试本身提供.

比如一个解析某类型文件的类, 可以把文件内容嵌入到测试代码里, 在测试的时候写入到临时文件, 测试结束再删除, 而不是从预定的地址直接读取.

23. 权衡测试成本

不写单元测试的代价很高, 但是写单元测试的代价同样很高. 要在这两者之间做适当的权衡, 如果用执行覆盖率来衡量, 业界标准通常在 80% 左右.

很典型的, 读写外部资源的错误处理和异常处理就很难达到百分百的执行覆盖率. 模拟数据库在事务处理到一半时发生故障并不是办不到, 但相对于进行大范围的代码审查, 代价可能太大了.

24. 安排测试优先次序

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

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

考虑下面的这个例子:

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");

26. 写测试用例重现 bug

每上报一个 bug, 都要写一个测试用例来重现这个 bug (即无法通过测试), 并用它作为成功修正代码的检验标准.

27. 了解局限

单元测试永远无法证明代码的正确性!!

一个跑失败的测试可能表明代码有错误, 但一个跑成功的测试什么也证明不了.

单元测试最有效的使用场合是在一个较低的层级验证并文档化需求, 以及 回归测试: 开发或重构代码时,不会破坏已有功能的正确性.


以下是一些良好的单元测试准则:

  1. 一个测试类只对应一个被测类。当前的测试类应该与其他的测试类、环境设置等没有任何依赖。

  2. 测试类的目录结构和被测试类的对应,这样便于快速找到错误位置。

  3. 一个测试方法只测试一个方法。同时,确保不要测试私有方法,它们是被封装起来的,并不是API。

  4. 测试用例的变量和方法都要有明确的含义。比如,将预期结果保存到 $expectedFoo 变量而不是只保存到 $foo。如果要测试很多复合结果,可使用组合变量名称诸如:$inputValue_NotNull$inputValue_ZeroData$inputValue_PastDate等等(这要看你的代码规范约定)。

  5. 测试用例具备可读性。测试用例代码应该遵循规范,像应用代码一样易于理解。以后的维护者会在阅读实现前去阅读你的测试,这将帮助他们在调试前理解被测类的逻辑。

  6. 测试用例干净整洁。程序中不要有流程控制语句(switchif 等)。一个好的测试用例处理顺序简单直接,准备数据,验证结果顺序,如有必要,使用子方法分解结构,让测试用例更加易读。如果是多场景,使用多个方法测试;例如,一个测试用例的方法代码长度最多满屏,不要有滚动条,大概在 1 到 20 行左右。如果测试代码太长,考虑拆开成多个方法,以避免混在一起相互干扰;

  7. 测试用例要验证预期的异常。PHP中使用 @expectedException,不要忽略它们。

  8. 测试用例不要连接数据库。如果测试中需要连接数据库,那么每个新的测试方法都应自主引导到临时数据库(使用 Setup/Teardown做准备)。如果不是必须连接数据库,请使用mock产生确定的数据。

  9. 测试用例不要连接网络资源。测试某个方法时无法确保第三方的有效性,诸如网络和设备的有效性,而应该使用mocks代替。

  10. 不要在自己的类中测试第三方的类库。类库应该由它们自己的测试用例来测试,这也是我们选择类库的原因。如果它们自己没有,应该考虑使用mock来模拟类库的输出结果,确保输入数据的确定性。我们不应该在测试自己类功能的时候,还要考虑第三方库的的功能。

  11. 测试用例要处理好边界情况,极限值 (max, min) 和null变量(即使抛异常)。你要确保这些问题状况永远都不会发生,甚至在维护时不使用测试用例。

  12. 测试用例在任何情况下都可运行,并且不需要配置和人工干预。

  13. 测试用例通过当前测试,并且易于改进。测试用例要能够支持代码的演变,如果很难维护或者代码太轻而不能细化,那就成了负担(很多人不写单元测试用例就是这个原因)。

  14. 测试用例的输入要具体。在PHP中,测试的方法不要使用 time()作为输入,最好使用date_format()创建具体格式的时间。再如:name = "Smith"; 不要用 name = "name"或者 name = "test";

  15. 测试用例不要使用@ignored或者被注释掉,切记切记。

  16. 测试用例帮忙验证代码架构。如果你不能测试某个方法或者类,那么你的设计就不够灵活。

  17. 测试用例可以运行在任何平台,而不仅是指定目标平台。不要指望一个特定设备或者硬件配置。不然你的测试用例会迁移困难,这样会导致你会禁用他们。不应该出现“在我的机器上没问题啊”这种情况。

  18. 测试用例运行速度快。慢的测试会把你拖垮,快的速度会鼓励你经常运行他们,它还能帮助你减少持续集成系统上的构建时间。慎用delay() 或者 sleep() ,比如只有在某些边缘情况下,比如等待通知或者基于时钟的方法;

  19. 把断言从逻辑中分离出来。断言应该用来检验结果,不应该执行逻辑操作的。

  20. 测试类不要包含私有的方法。私有方法都是一些具体的实现,不应该包含在单元测试里。

  21. 一个测试不要超过一个模拟(mock对象)。不然如何消除错误和不一致性。

  22. 保持你的测试是幂等的。测试程序应该能运行多次保持结果一致,不论运行一次还是一百万次,它的效果都应该是一样的。并且,在测试过程中,我们不应该改变任何的数据或者添加任何东西。

关于单元测试,网上多为描述具体实战和其重要性,很少针对单元测试方式和原则做进一步说明,而实际工作过程中很多开发者不知道应该测试,Jean-baptiste Rieu写了《Unit Testing Checklist: Keep Your Tests Useful and Avoid Big Mistakes》一文,他对单元测试中的原则和思想整理为一个checklist,相信对开发者有很大帮助。

1. 为何使用单元测试

它可以测试现有以及未来的功能模块,保证了代码质量。它强制你书写具有可测性,低耦合的代码。这比手工回归测试廉价的多。它将提高代码可行度,协助团队工作。

2. 测试步骤

单元测试是验证你代码的一些常用方法集合。按照下面的步骤操作是个不错的方法:

  • 写被测类的API;

  • 写一个方法测试API;

  • 实现这个API;

  • 执行单元测试;

3. 测试原则(检查清单)

为何需要个检查清单?单元测试比实际实现可能还要难一些,它强迫你考虑清楚一些事情。但单元测试本身应该简单、直接、易用和易于维护。你还要知道何时停止写测试并且开始写实现。使用这个检查清单能够确保有效测试且达到目标,该清单能帮你避免一些明显的错误。

  • 一个测试类只能对应一个被测类。你要测试的是对应类API的正确性,结果是所期望的。

  • 每次只测试一个方法。确保不要测试私有方法,它们是被封装起来的,并不是API。

  • 测试用例的变量和方法都要有明确的含义。比如,将预期结果保存到 expectedFoo 变量而不是只保存到 foo。如果要测试很多复合结果,可使用组合变量名称诸如:inputValue_NotNullinputValue_ZeroDatainputValue_PastDate等等 (这要看你的代码规范约定)。

  • 测试用例易读。以后的维护者会在阅读实现前去阅读你的测试,这将帮助他们在调试前理解被测类的逻辑。

  • 测试用例干净整洁。

    • 程序中不要有流程控制语句(switchif 等)。一个好的测试用例处理顺序简单直接,准备数据,验证结果顺序,如有必要,使用子方法分解结构,让测试用例更加易读。如果是多场景,使用多个方法测试;

    • 例如,一个测试用例的方法代码长度最多满屏,不要有滚动条,大概在 1 到 20 行左右。如果测试代码太长,考虑拆开成多个方法,以避免混在一起相互干扰;

  • 测试用例要验证预期的异常。Java中使用 @Test(expected=MyException.class)。

  • 测试用例不要连接数据库。或者说,如果测试中需要连接数据库操作,必须使用mock,每个新的测试方法都自主引导到临时数据库(使用 Setup/Teardown做准备)。

  • 测试用例不要连接网络资源。测试某个方法时无法确保第三方诸如网络和设备的有效性 (使用mocks)。

  • 测试用例要处理好边界情况,极限值 (maxmin) 和null变量(即使抛异常)。你要确保这些问题状况永远都不会发生,甚至在维护时不使用测试用例。

  • 测试用例在任何情况下都可运行,并且不需要配置和人工干预。

  • 测试用例通过当前测试,并且易于改进。测试用例要能够支持代码的演变,如果很难维护或者代码太轻而不能细化,那就成了负担(很多人不写单元测试用例就是这个原因)。

  • 测试用例要具体。在Java中,测试的方法不要使用 Date()作为输入,最好使用Calendar创建具体格式的时间(别忘了设置时区)。再如:name = “Smith”; 不要用 name = “name”或者 name = “test”;

  • 测试用例使用mock来模拟复杂的类结构或方法。

    • 记住一次只测试一个类;

    • 不要在自己的类中测试第三方的类库. 类库应该由它们自己的测试用例来测试(这也是选择选择类库的一个好方式);

  • 测试用例不要使用@ignored或者被注释掉,切记切记。

  • 测试用例帮我验证了代码架构。如果你不能测试某个方法或者类,那么你的设计就不够灵活。

  • 测试用例可以运行在任何平台,而不仅是指定目标平台。不要指望一个特定设备或者硬件配置。不然你的测试用例会迁移困难,这样会导致你会禁用他们。

  • 测试用例运行速度快。

    • 慢的测试会把你拖垮,快的速度会鼓励你经常运行他们,它还能帮助你减少持续集成系统上的构建时间;

    • 使用测试运行程序时允许你一次启动一个测试。慎用“delay” 或者 “sleep” ,比如只有在某些边缘情况下,比如等待通知或者基于时钟的方法;


如对本文有疑问,请提交到交流论坛,广大热心网友会为你解答!! 点击进入论坛


发表评论 (128人查看0条评论)
请自觉遵守互联网相关的政策法规,严禁发布色情、暴力、反动的言论。
用户名: 验证码: 点击我更换图片
最新评论
------分隔线----------------------------