7.正确性的软件构造
7.1 健壮性和正确性
健壮性:系统在不正常输入或不正常外部环境下仍能够表现正常的程度。(对自己的代码要保守,对用户的行为要开放)实现方法:封闭实现细节,限定用户的恶意行为。
正确性:程序按照spec加以执行的能力,是最重要的质量指标。
正确性倾向于直接报错(error),健壮性则倾向于容错(fault-tolerance)
对外的接口,倾向于健壮;对内的实现,倾向于正确。
可靠性=健壮性+正确性
7.1.1衡量正确性和健壮性
Mean time between failures (MTBF,平均失效间隔时间)
7.2 错误与异常处理
Java中的错误与异常机制,异常的处理
7.2.1 Java中的错误和异常
内部错误:程序员通常无能为力,一旦发生,想办法让程序优雅的结束
异常:你自己程序导致的问题,可以捕获、可以处理。
错误的种类:用户输入错误、设备错误、物理限制
7.2.2 异常的处理
异常:程序执行中的非正常事件,程序无法再按预想的流程执行
- 异常的分类
异常被定义为一种可抛出的类型 - RuntimeException
运行时异常,是程序源代码中引入的故障所造成的 - checked和unchecked的异常
unchecked异常:在代码中就没有catch也没有throw,编译的时候会通过,但是运行时如果出错会直接停止。编译器可帮助检查你的程序是否已抛出或处理了可能的异常
checked异常:被throw或catch的异常。
如果客户端可以通过其他的方法恢复异常,那么采用checked exception
如果客户端对出现的这种异常无能为力,那么采用unchecked exception
在编程的时候尽量使用unchecked异常,这样方便确认出错的位置。 - 异常处理
可以通过throw来处理异常,但是如果最终的调用者没有handler来处理被抛出的checked exception,程序就终止执行。
程序员必须在方法的spec中明确写清本方法会抛出的所有checked exception,以便于调用该方法的client加以处理。
如果子类型中override了父类型中的函数,那么子类型中方法抛出的异常不能比父类型抛出的异常类型更宽泛。
子类型方法可以抛出更具体的异常,也可以不抛出任何异常。
如果父类型的方法未抛出异常,那么子类型的方法也不能抛出异常。 - 异常的定义
定义一个checked异常:(重写四个方法即可,基本都是super)
自定义一个unchecked异常:(相较于checked内容更加简单)
- 捕获异常
使用try-catch捕获异常。如果选择了throw那么即是将异常传递给调用者进行处理。 - 重新抛出和链接异常
catch操作后既可以用来处理异常,也可以用来改变异常的类型,方便用户获取信息。
finally操作:无论异常有没有被捕获到,都会执行。用来关闭或清除之前申请的资源。 - 调用栈分析
将方法之间的调用关系,一层一层的输出。后调用的先输出。
7.3 断言与防御式编程
检查前置条件是防御式编程的典型形式
7.3.1 断言
断言在开发阶段的代码中嵌入,用于检验某些假设是否成立。
在检查表示不变量、方法的前置条件、后置条件的时候需要使用断言。由于在程序投入使用时断言将不再被执行,所以,应该避免断言语句执行了某一个关键代码。
内部错误使用断言,外部错误使用异常机制处理。
断言提升了正确性,异常机制提升了健壮性。
7.3.2 防御式编程
对来自外部的数据源要仔细检查,例如:文件、网络数据、用户输入等。对每个函数的输入参数合法性要做仔细检查,并决定如何处理非法输入。
类的public方法接收到的外部数据都应被认为是dirty的,需要处理干净再传递到private方法——隔离舱。
- Debug过程注意的
使用具有进攻性的编程方式,注意删除Debug过程中的代码。
7.4 Debug专题
7.4.1 使用Log调试
Java自带的log库:java.util.logging。
通过设定日志级别来确定要log哪些信息、log结果可被多种渠道加以处理,可通过设定条件进行过滤,并输出为多种格式、可使用层次化的多个日志记录器。
缺省:输出到控制台。除了ConsoleHandler,还可以设定其他的handlers:
StreamHandler、FileHandler、SocketHandler、MemoryHandler
7.5 软件测试与测试优先的编程
重点:测试用例,单元测试,黑盒测试(等价类划分,边界值分析)
7.5.1 软件测试
提高软件质量,提高代码的正确度。
- 测试等级
单元测试、集成测试、系统测试 - 黑盒测试
对程序外部外部表现出来的行为进行测试
7.5.2 测试用例(Test Case)
测试用例:输入+执行条件+期望结果
7.5.3 测试优先的编程
先写spec,再写符合spec的测试用例。写测试用例,就是理解、修正、完善spec设计的过程。
spec描述了方法预期的输入和输出。先写测试会节省大量时间。
7.5.4 单元测试
针对软件的最小单元模型开展测试,隔离各个模块,容易定位错误和调试。
7.5.5 使用JUnit自动生成单元测试
assertTrue,assertEqual是十分常用的。
测试的文件路径应该和源文件的文件路径一致。
7.5.6 黑盒测试
用于检查代码的功能,不关心内部实现细节。
- 等价类划分
基于等价类划分的测试:将被测函数的输入域划分为等价类,从等价类中导出测试用例。
针对每个输入数据需要满足的约束条件,划分等价类。基于假设:相似的输入会产生相似的行为。故可以选择等价类中的一个代表进行测试即可。
例1:
高精度乘法,可以将两个数中的任何一个数分为以下七类:大负数,小负数,-1,0,1,小正数,大正数
因此两个数有如下的测试组合:
例2:
取最大值的方法,划分为a>b a<b a=b三种等价类。 - 在分区的时候包含边界
大量的错误发生在边界上而不是取值范围的中间。同样的边界的两侧也需要考虑。
在例2中,可以添加上整数的最大值和最小值进行比较。
7.5.7 测试覆盖度
代码覆盖度:已有的测试用例有多大程度覆盖了被测程序
测试效果:路径覆盖>分支覆盖>语句覆盖
测试难度:路径覆盖>分支覆盖>语句覆盖
路径覆盖:每一个分支全部测试一遍。但是路径量巨大,难以覆盖全面。
7.5.8 测试策略的文档
测试策略(根据什么来选择测试用例)非常重要,需要在程序中显式记录下来 。
这是一个反转字符串方法的测试,可以看到:
将字符串长度分为0,1,>1三种情况
将开始的位置分为0,1,>1但是<字符串长度,字符串长度-1(最后一位)和等于字符串长度(超了一位)、
被反转的部分字符串的长度为:0,1,奇数,偶数。