【HIT-SC-MEMO6】哈工大2022软件构造 复习笔记6

六、健壮性

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 exceptionUnchecked 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
  • 提高可维护性
AssertionException
提高“正确性”提高“健壮性”
错误/异常处理是提高健壮性,处理外部行为;断言是提高正确性,处理内部行为使用异常来处理你“预料到可以发生”的不正常情况;使用断言处理“绝不应该发生”的情况
内部行为外部行为
处理“绝不应该发生”的情况处理“可以预料到会发生”的情况

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将日志存储在不同的地方。
日志级别(从高到低):SEVEREWARNINGINFOCONFIGFINEFINERFINEST
设定级别: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类型:StreamHandlerConsoleHandlerFileHandlerSocketHandlerMemoryHandler
  • 设定处理器: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
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值