software construction six

六、健壮性
6.1 健壮性和正确性的含义和区别
健壮性:系统在不正常输入或不正常外部环境下仍能够表现正常的程度
面向健壮性的编程
处理未期望的行为和错误终止
即使终止执行,也要准确/无歧义的向用户展示全面的错误信息
错误信息有助于进行debug
Robustness principle (Postel’s Law):对自己的代码要保守,对用户的行为要开放
正确性:程序按照spec加以执行的能力,是最重要的质量指标!
区别

正确性:永不给用户错误的结果
健壮性:尽可能保持软件运行而不是总是退出
正确性倾向于直接报错(error),健壮性则倾向于容错(fault-tolerance)
6.2 错误与异常处理

Throwable

Java中的内部错误(Error) & 异常(Exception)
Error 内部错误 and Exception 异常
内部错误:程序员通常无能为力,一旦发生,想办法让程序优雅的结束
用户输入错误
设备错误
物理限制
异常:你自己程序导致的问题,可以捕获、可以处理
由于程序员对Error通常无法预料无法解决,因此重点关注可被解决的Exception

异常处理
Java中Exception可以被分为两个部分,蓝色的运行时异常和绿色的其他异常。

运行时异常:由程序员在代码里处理不当造成,在源代码中引入了故障,而如果在代码中提前进行验证,这些故障就可以避免。动态类型检查的时候会发现这种异常,而一旦出现,代码就必然有错误,可以通过调试解决。
其他异常:由外部原因造成,程序员无法完全控制的外在问题所导致的,即使在代码中提前加以验证,也无法完全避免失效发生。
Java’s exception handling consists of three operations:
Declaring exceptions (throws)声明“本方法可能会发生XX异常”
Throwing an exception (throw)抛出XX异常
Catching an exception (try, catch, finally) 捕获并处理XX异常
Checked & Unchecked Exceptions区别
Checked exception Unchecked exception
Basic 必须被显式地捕获或者传递 (try-catch-finally-throw),否则编译器无法通过,在静态类型检查时就会报错 异常可以不必捕获或抛出,编译器不去检查,不会给出任何错误提示
Class of Exception 继承自Exception类(上图中的绿色部分) 继承自RuntimeException类(上图中的蓝色部分)
Handling 从异常发生的现场获取详细的信息,利用异常返回的信息来明确操作失败的原因,并加以合理的恢复处理 简单打印异常信息,无法再继续处理
Appearance 代码看起来复杂,正常逻辑代码和异常处理代码混在一起 清晰,简单
选取checked exception还是unchecked exception可遵循下面的原则:

checked exception:如果客户端可以通过其他的方法恢复异常,而对开发者来说错误可预料但不可预防,它的出现已经脱离了程序能够掌控的范围。
unchecked exception:如果客户端对出现的这种异常无能为力,而对开发者来说错误可预料可预防,它可以通过调整程序来避免出现。
Checked异常的处理机制
自定义异常类
可以选择创建自定义异常类型:

public class FooException extends Exception {
public FooException() { super(); }
public FooException(String message) { super(message); }
public FooException(String message, Throwable cause) { super(message, cause); }
public FooException(Throwable cause) { super(cause); }
}

声明异常 & 抛出异常
使用throw关键字抛出异常,如:throw new EOFException();
String readData(Scanner in) throws EOFException // 声明:本函数可能发生该异常
{
. . .
while (. . .)
{
if (!in.hasNext()) // EOF encountered
{
if (n < len)
throw new EOFException(); // 异常在这里发生了
}
. . .
}
return s;
}

捕获异常 & 处理异常
可以使用try-catch语法对抛出的异常进行处理,也可以用throws语法将异常抛给上一级调用,然后在上一级中使用try-catch处理。

public static void fun() throws IOException { // 已声明可能抛出的异常

}
public static void main(String args[]) {
try{
fun();
} catch (IOExeption e) { // 延迟到此处捕获
e.printStackTrace();
}
}

所以,try-chtch所捕获到的异常可能有两个来源,一是自己内部的代码产生的,二是调用了其他的方法,并且该方法未处理抛给了本方法。

本来catch语句下面是用来做exception handling的,但也可以在catch里抛出异常
这么做的目的是:更改exception的类型,更方便client端获取错误信息并处理
try {

}
catch (AException e) { // 捕获到A异常
// 抛出B异常,并带上异常消息
throw new BException( " xxx error:" + e. getMessage());
}

Finally
处理异常时释放资源:当异常抛出时,方法中正常执行的代码被终止,如果异常发生前曾申请过某些资源,那么异常发生后这些资源要被恰当的清理。
所以形成了try-catch-finally结构。不管程序是否碰到异常,finally都会被执行。

LSP & 异常
如果子类型中override了父类型中的方法,那么子类型中方法抛出的异常不能比父类型抛出的异常类型更宽泛,异常不能逆变
子类型方法可以抛出更具体的异常,也可以不抛出任何异常,异常可以协变
如果父类型的方法未抛出异常,那么子类型的方法也不能抛出异常
目的还是为了能够让客户端能够用统一的方式处理不同类型的对象。

6.3 断言与防御式编程
防御式编程的基本思想
最好的防御就是不要引入bug
如果无法避免
尝试着将bug限制在最小的范围内
限定在一个方法内部,不扩散
Fail fast:尽快失败,就容易发现、越早修复
断言Assertion
断言:

在开发阶段的代码中嵌入,检验某些“假设”是否成立。若成立,表明程序运行正常,否则表明存在错误。
断言即是对代码中程序员所做假设的文档化,也不会影响运行时性能(在实际使用时,assertion都会被disabled)
语法:assert condition : message;
所构造message在发生错误时显示给用户,便于快速发现错误所在
作用:

最高效、快速地找出/改正bug
提高可维护性
Assertion Exception
提高“正确性” 提高“健壮性”
错误/异常处理是提高健壮性,处理外部行为;断言是提高正确性,处理内部行为 使用异常来处理你“预料到可以发生”的不正常情况;使用断言处理“绝不应该发生”的情况
内部行为 外部行为
处理“绝不应该发生”的情况 处理“可以预料到会发生”的情况
assert使用场所:

内部不变量:判断某个局部变量应该满足的条件,assert x > 0
表示不变量:checkRep()
控制流不变量:例如,若不想让程序走向switch-case的某个分支,则可以用断言直接在分支上assert false;
方法的前置条件:判断传入参数是否满足前置条件
方法的后置条件:判断结果时候满足后置条件
6.4 代码调试
防御式编程–>测试–>调试

调试的基本过程和方法
调试 Debug:

Debug的目的是寻求错误的根源并消除它
Debug占用了大量的开发时间
Debug是测试的后续步骤:test发现问题,debug消除问题
测试步骤:

常用方法:假设-检验

Diagnose:

Instrumentation 测量
即最基本的打印内容,把要观察的对象全部打印到控制台(最后要把这些语句都删掉)。
可使用log功能统一管理。
Divide and Conquer 分治
防狼围栏算法
Slicing 切片
寻找特定有关于“错误变量”的代码部分

Focus on difference 寻找差异
充分利用版本控制系统,找出在哪个commit之后出现了bug症状
Delta Debugging基于差异的调试:两个测试用例,分别通过/未通过;通过查找二者所覆盖的代码之间的差异,快速定位出可能造成bug的代码行。
Symbolic Debugging 符号
符号化执行
不需输入特定的值,使用“符号值”(而非“实际值”)作为输入,解释器模拟程序执行,获得每个变量的“符号化表达式”,从而可判断是否执行正确。

Debugger 调试器
使用日志开展调试
日志管理工具:

JDK logging
Apache Log4j
java.util.logging
通过设定日志级别来确定要log哪些信息,也可以通过Handler将日志存储在不同的地方。
日志级别(从高到低):SEVERE、WARNING、INFO、CONFIG、FINE、FINER、FINEST
设定级别:logger.setLevel(Level.INFO);

Logger

可以设定全局的logger,但是会造成信息混乱的后果,于是需要定义自己的logger,然后在程序中使用它。
import java.util.logging.*;
private static final Logger myLogger = Logger.getLogger(“com.mycompany.myapp”);
//often using class name as logger name
//或者用这种方式
public class LogTest {
static String strClassName = LogTest.class.getName(); //get class name
static Logger myLogger = Logger.getLogger(strClassName);
// using class name as logger name

myLogger.info(“ XXXX ”);
}

Logging Handlers

Handler是日志的输出位置,缺省输出到控制台。
日志处理器也需要设定日志级别。
Handler类型:StreamHandler、ConsoleHandler、FileHandler、SocketHandler、MemoryHandler…
设定处理器:logger.addHandler(new FileHandler(“test.txt”)
日志的格式:SimpleFormatter、…
设定格式:fileHandler.setFormatter(new SimpleFormatter())
6.5 软件测试与测试优先的编程
Unit单元测试:function、class
Integration集成测试:classes、packages、components、subsystems
System系统测试:system
Regression回归测试:修改后再测试
Acceptance验收测试
Static/Dynamic 测试

Static:
不执行程序
Reviews
walkthroughs 预排/演练/走查
inspections 视察
Dynamic:
执行程序,有测试用例
Debugger
使用JUnit进行单元测试(Unit Test)
单元测试:

针对软件最小单元
隔离模块
容易定位错误,容易调试
JUnit在测试方法前使用@Testannotation来表明这是一个JUnit测试方法。如果要在测试开始之前做一些准备则在准备方法前添加@Beforeannotation,如果要在测试结束后做一些收尾工作则在收尾方法前添加@Afterannotation。

JUnit使用的是断言机制来完成测试,常用的有三种测试方法:assertEquals()、assertTrue()、assertFalse()。

黑盒测试
白盒测试:对程序内部代码结构的测试
黑盒测试:对程序外部表现出来的行为的测试

黑盒测试:

检查代码功能
不关心内部细节
检查程序是否符合规约
用尽可能少的测试用例尽快运行、尽可能大发现程序错误

等价类划分和边界值分析
Equivalence Partitioning 等价类划分:

针对每个输入数据需要满足的约束条件,划分等价类,导出测试用例
每个等价类代表着对输入约束加以“满足/违反”的“有效/无效”数据集合
基于假设:相似的输入会展示相似的行为
因此,每个等价类选一个做代表即可,可以降低测试用例数量
Boundary Value Analysis 边界值分析

大量错误出现在输入域的边界而不是中央
对等价类划分的补充
覆盖划分的方法

Full Cartesian product:笛卡尔积、全覆盖
测试完备、用例数量多、测试代价高
Cover each Part:覆盖每个取值,最少1次即可
测试用例少、代价低、测试覆盖度不够高
例子:大整数乘法

二维输入空间
两个数的正负性:++,±,-+,–
特殊值:0,1,-1
很大的数

例:Max()

大于、等于、小于

测试覆盖度
Code Coverage代码覆盖度:已有的测试用例有多大程度覆盖了被测程序 。

代码覆盖度越低,测试越不充分;
代码覆盖度越高,测试代价越高。
测试覆盖种类:

Function 函数覆盖
Statement 语句覆盖
Branch 分支覆盖
if、while、switch-case、for
Condition条件覆盖 ≈ 分支覆盖
Path路径覆盖
分支的组合 = 路径
测试效果:路径覆盖 > 分支覆盖 > 语句覆盖
测试难度:路径覆盖 > 分支覆盖 > 语句覆盖

100%语句覆盖是common(正常)目标
100%分支覆盖是desirable(令人满意的),arduous(很难实现),有些行业有更高标准
100%路径覆盖是infeasible(不可实行的)

以注释的形式撰写测试策略
在程序中显式记录测试策略(根据什么来选择测试用例)
在代码评审的过程中,其他人可以理解你的测试,并评判测试是否足够充分

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值