Software Testing 软件测试是提高软件质量的重要手段。但软件质量取决于软件设计,而不取决于软件测试。
What is a good test? 能发现错误,不冗余,最佳特性,别太复杂也别太简单
单元测试 集成测试 系统测试 验收测试
静态测试 vs 动态测试:靠眼睛看 vs 撸起袖子使劲干
白盒测试:对程序内部代码结构的测试(关注程序的执行路径/情况开发完才可以做)
黑盒测试:对程序外部表现出来的行为的测试(可在运行(开发)前做)
好的测试用例的特点:最可能发现错误,不重复、不冗余,最有效,既不简单也不复杂
测试优先的编程(Test-first programming): Write the tests before you write the code.
Process:
– Write a specification for the function. 先写spec
– Write tests that exercise the specification. 再写符合spec的测试用例
– Write the actual code. Once your code passes the tests you wrote, you’re done. 写代码、执行测试、有问题再改、再执行测试用例,直到通过它
测试驱动开发 (TDD) 是一个开发过程,它依赖于非常短的开发周期的重复:将需求转化为非常具体的测试用例,然后改进软件以通过新的测试。 它与允许添加未被证明满足需求的软件的软件开发相反。
Unit Testing 单元测试(黑盒):
针对软件的最小单元模型开展测试,隔离各个模块,容易定位错误和调试
黑盒测试:用于检查代码的功能,不关心内 部实现细节
基于等价类划分的测试:将被测函数的输入域划分为等价类, 从等价类中导出测试用例。
针对每个输 入数据需要满足的约束条件,划分等价类
每个等价类代表着对输入约束加以满足/违反的有效 /无效数据的集合
基于的假设:相似的输入,将会展示相似的行为。故可从每个等价类中选一个代表作为测试用例即可。从而可以降低测试用例数量
大量的错误发生在输入域的“边界 ”而非中央。边界值分析方法是对等价类划分方法的补充。(补充测试用例)
White-box Testing
黑盒测试完 全从函数spec导出测试用例,不考虑函数内部实现。白盒测试要考虑内部实现细节。
根据程序执行路径设计测试用例(确保程序的每一条路径都被执行)。白盒测试一般较早执行。
独立/基本路径测试:对程序所有执行路径进行等价类划分,找出有代表性的最简单的路径(例如循环只需执行1次),设计测试用例使每一条基本路径被至少覆盖1次
代码覆盖度:已有的测试用例有多大程度覆盖了被测程序。代码覆盖度越低,测试越不充分。但要做到很高的代码覆盖度,需要更多的测试用例,测试代价高。
测试效果:路径覆盖>分支覆盖>语句覆盖
测试难度:路径覆盖>分支覆 盖>语句覆盖
但路径数量巨大,难以全覆盖
实际当中,根据预先设定的覆盖度标准,逐步增加测试用例的数量, 直到覆盖度达到标准(例如语句覆盖100%、路径覆盖90%)。
Regression testing 回归测试
回归测试:一旦程序被修改,重新执行之前的所有测试
自动化回归测试
一旦发现bug,要马上写一个可重现该bug的测试用例,并将其加入测试库。
面向正确性与健壮性的软件构造
正确性倾向于直接报错(error),健壮性则倾向于容错(fault-tolerance)
正确性: 让开发者变得更容易:用户输入错误,直接结束。 (不满足precondition的调用)
健壮性: 让用户变得更容易:出错也可 以容忍,程序内部已有容错机 制
对外的接口,倾向于健壮;对内的实现,倾向于正确
如何衡量稳健性和正确性
外部观察角度:平均故障间隔 时间(平均无故障运行时间),是指相邻两次故障之间的平均工作时间。对于可修复系统,是指导致系统不可使用的failure。出现问题,但系统仍可运行,则不视为failure
MTBF用于描述可修复系统的平均无故障运行时间 ,MTTF(故障前平均时间)描述不可修复系统的故障前平均时间
内部观察角度:残余缺陷率 : 每千行代码中遗留的bug的数量
Java 中的错误和异常
内部错误:程序员通常无能为力,一旦发生,想办法让程序优雅的结束。异常:你自己程序导致的问题,可以捕获、可以处理
Sorts of errors:用户输入错误,设备错误,物理限制
Exception Handling(既然Error我们无能为力, 那就转向关注我们能处理的Exception)
若找不 到异常处理程序,整个系统完全退出
运行时异常:由程序员在代码里处理不当造成;其他异常:由外部原因造成
运行时异常,是程序 源代码中引入的故障所造成的,如果在代码中提前进行验证,这些故障就可以避免。非运行时异常,是程序员无法完全控制的外在问题所导致的 ,即使在代码中提前加以验证(如:文件是否存在),也无法完全避免失效发 生。
检查和未检查异常(这是从异常处理机制的角度所做的分类 异常被谁check?——编译器、程序员)
Checked Exceptions:编译器可帮助检查你的程序是否已抛出或处理了可能的异常。
Unchecked exceptions:可以不处理,编译没 问题,但执行时出现 就导致程序失败,代 表程序中的潜在bug 类似于编程语言中的 dynamic type checking
Checked exceptions:必须捕获并指定错误 处理器handler,否则 编译无法通过 类似于编程语言中的 static type checking
Common Unchecked Exception Classes在编程和编译的时候,IDE与编 译器均不会给出任何错误提示
Checked Exception Handling Operations异常处理中使用了五个关键字:try – catch – finally – throws – throw
throws 声明“本方法可能会发生XX异常
throw 抛出XX异常
try, catch, finally 捕获并处理XX异常
当要决定是采用checked exception还是unchecked exception的时候,问一个问题:“如果这种异常一旦抛出,client会做 怎样的补救?”
– 如果客户端可以通过其他的方法恢复异常,那么采用checked exception;
– 如果客户端对出现的这种异常无能为力,那么采用unchecked exception;
– 异常出现的时候,要做一些试图恢复它的动作而不要仅仅的打印它的信息;
尽量使用unchecked exception来处理编程错误:因为unchecked exception不用使客户端代码显式的处理它们,它们自己会在出现的地 方挂起程序并打印出异常信息。
错误可预 料,但无法预防,但可以有手段从中恢复,此时使用checked exception。如果做 不到这一点,则使用unchecked exception。
通过抛出声明检查异常
程序员必 须在方法的spec中明确写清本方法会抛出的所有checked exception, 以便于调用该方法的client加以处理
如果子类型中override了父类型中 的函数,那么子类型中方法抛出的异常不能比父类型抛出的异常类型 更宽泛
子类型方法可以抛出更具体的异 常,也可以不抛出任何异常
如果父类型的方法未抛出 异常,那么子类型的方法也不能抛出异常。
参见LSP原则 目标是子类型多态:客户端可用统一的方式处理不 同类型的对象,子类型可替代父类型
Finally部分的代码,是否捕获异常都会被执行
断言
最好的防御就是不要引入bug。如果无法避免,尝试着将bug限制在最小的范围内。限定在一个方法内部,不扩散。尽快失败,就容易发现、越早修复。检查前置条件是防御式编程的一种典型形式。
断言:在开发阶段的代码中嵌入,检验某些“假设”是否 成立。若成立,表明程序运行正常,否则表明存在错误
断言主要用于开发阶段,避免引入 和帮助发现bug。实际运行阶段, 不再使用断言。避免降低性能。使用断言的主要目的是 为了在开发阶段调试程序、尽快避免错误。
断言非常影响运行时的性能
如果参数来 自于外部(不受自己控制),使用异常处理。如果来自于自己 所写的其他代码,可以使用断 言来帮助发现错误。
断言和异常处理都可以处理同样的错误。开发阶段用断言尽可能消除bugs 在发行版本里用异常处理机制处理漏掉的错误。
防御式编程
(1)保护程序免受无效输入:对来自外部的数 据源要仔细检查,例如:文件、网络数据、用户输入等。对每个函数的输入 参数合法性要做仔细检查,并决定如何处理非法输入。
(2) Barricade 设置路障:类的public方法 接收到的外部数据都应被认为是dirty的,需要处理干净再传递到 private方法——隔离舱。操作间技术。“隔离舱”外部的函数应使用异常处理,“隔离舱”内的函数应使用 断言。Proxy设计模式?——隔离