83417807 2020-06-15
理论上,任何代码提交前都应该完整跑一遍所有测试套件。保持测试代码执行符合预期,这样能够缩短迭代开发周期。
测试套件通常是定期执行的,执行过程必须完全自动化才有意义。输出结果需要人工检查的测试不是一个好的单元测试。
对开发环境进行配置,最好是敲一条命令或是点击一个按钮就能把单个测试用例或测试套件跑起来。
对执行的测试进行覆盖率分析,得到精确的代码执行覆盖率,并调查哪些代码未被执行。
每个开发人员在提交前都应该保证新的测试用例执行成功,当有代码提交时,现有测试用例也都能跑通。
如果一个定期执行的测试用例执行失败,整个团队应该放下手上的工作先解决这个问题。
单元测试即类(Class)的测试。一个“测试类”应该只对应于一个“被测类”,并且“被测类”的行为应该被隔离测试。必须谨慎避免使用单元测试框架来测试整个程序的工作流,这样的测试即低效又难维护。工作流测试(译注:指跨模块、类的数据流测试)有它自己的地盘,但它绝不是单元测试,必须单独建立和执行。
再简单的测试也远远胜过完全没有测试。一个简单的“测试类”会促使建立“被测类”基本的测试骨架,可以对构建环境、单元测试环境、执行环境以及覆盖率分析工具等有效性进行检查,同时也可以证明“被测类”能够被整合和调用。
下面便是单元测试版的“Hello, world!”:
void testDefaultConstruction() { Foo foo = new Foo(); assertNotNull(foo); }
为了保证测试稳定可靠且便于维护,测试用例之间决不能有相互依赖,也不能依赖执行的先后次序。(实际上TestNG有提供依赖的功能,或许某些场景也需要依赖。)
[译注:有意翻译该规则,个人认为本条规则值得商榷,大部分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.
确保每个方法只测试“被测类”的一个明确特性,并相应的命名测试方法。典型的命名俗定是“test[MethodName]”,比如“testSaveAs()”、“testAddListener()”、“testDeleteProperty()”等。
单元测试可以被定义为通过类的公有API对类进行测试。一些测试工具允许测试一个类的私有成员,但这种做法应该避免,它让测试变得繁琐而且更难维护。如果有私有成员确实需要进行直接测试,可以考虑把它重构到工具类的公有方法中。但要注意这么做是为了改善设计,而不是帮助测试。
站在第三方使用者的角度,测试一个类是否满足规定的需求。并设法让它出问题。
毕竟被测试类是程序员自写自测的,应该在最复杂的逻辑部分多花些精力测试。
通常建议所有重要的函数都应该被测试到,一些芝麻方法比如简单的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 }
因此建议测试所有方法,毕竟芝麻用例也容易测试。
区别对待“执行覆盖率”和“实际测试覆盖率”。测试的最初目标应该是确保较高的执行覆盖率,这样能保证代码在 少量参数值输入时能执行成功。一旦执行覆盖率就绪,就应该开始改进测试覆盖率了。注意,实际的测试覆盖率很难衡量(而且往往趋近于0%)。
思考以下公有方法:
void setLength(double length);
调用“setLength(1.0)”你可能会得到100%的执行覆盖率。但要达到100%的实际测试覆盖率,有多少个 double浮点数这个方法就必须被调用多少次,并且要一一验证行为的正确性。这无疑是不可能的任务。
确保参数边界值均被覆盖。对于数字,测试负数、0、正数、最小值、最大值、NaN(非数字)、无穷大等;对于字符串,测试空字符串、单字符、非ASCII字符串、多字节字符串等;对于集合类型,测试空、1、第一个、最后一个等;对于日期,测试1月1号、2月29号、12月31号等。被测试的类本身也会暗示一些特定情况下的边界值。 要点是尽可能彻底的测试这些边界值,因为它们都是主要“疑犯”。
当边界值都覆盖了,另一个能进一步改善测试覆盖率的简单方法就是生成随机参数,这样每次执行测试都会有不同的输入。
想要做到这点,需要提供一个用来生成基本类型(如:浮点数、整型、字符串、日期等)随机值的工具类。生成器应该覆盖各种类型的所有取值范围。
如果测试时间比较短,可以考虑再裹上一层循环,覆盖尽可能多的输入组合。下面的例子是验证两次转换“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); } }
在测试模式下,有时会情不自禁的滥用断言。这种做法会导致维护更困难,需要极力避免。仅对测试方法名指示的特性进行明确测试。
因为对于一般性代码而言,保证测试代码尽可能少是一个重要目标。
应该总是优先使用“assertEquals(a, b)”而不是“assertTrue(a == b)”,因为前者会给出更有意义的测试失败信息。在事先不确定输入值的情况下,这条规则尤为重要,比如之前使用随机参数值组合的例子。
反向测试是指刻意编写问题代码,来验证鲁棒性和能否正确的处理错误。
假设如下方法的参数如果传进去的是负数,会立马抛出异常:
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 }
编写和维护单元测试的代价是很高的,减少代码中的公有接口和循环复杂度是降低成本和使高覆盖率测试代码更易于编写和维护的有效方法。
一些建议:
对于黑箱而言,所有方法都必须一视同仁的进行测试。考虑以下简短的例子:
public void scale(double x0, double y0, double scaleFactor) { // scaling logic } public void scale(double x0, double y0) { scale(x0, y0, 1.0); }
单元测试代码不应该假定外部的执行环境,以便在任何时候和任何地方都能执行。为了向测试提供必需的资源,这些资源应该由测试本身提供。
比如:一个解析某类型文件的类,可以把文件内容嵌入到测试代码里。在测试的时候写入到临时文件,测试结束再删除,而不是从预定的地址直接读取。
不写单元测试的代价很高,但是写单元测试的代价同样很高。要在这两者之间做适当的权衡,如果用执行覆盖率来衡量,业界标准通常在80%左右。
很典型的,读写外部资源的错误处理和异常处理就很难达到百分百的执行覆盖率。模拟数据库在事务处理到一半时发生故障并不是办不到,但相对于进行大范围的代码审查,代价可能太大了。
单元测试是典型的自底向上过程,如果没有足够的资源测试一个系统的所有模块,就应该先把重点放在较底层的模块。
考虑下面的这个例子:
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");
每上报一个Bug,都要写一个测试用例来重现这个Bug(即无法通过测试),并用它作为成功修正代码的检验标准。
单元测试永远无法证明代码的正确性!!
一个跑失败的测试可能表明代码有错误,但一个跑成功的测试什么也证明不了。
单元测试最有效的使用场合是在一个较低的层级验证并文档化需求,以及回归测试:开发或重构代码,不会破坏已有功能的正确性。
以上内容就是本篇的全部内容以上内容希望对你有帮助,有被帮助到的朋友欢迎点赞,评论。
如果对软件测试、接口测试、自动化测试、面试经验交流。感兴趣可以关注博主,加入我们会有同行一起技术交流哦。