哈工大软件构造复习5

1 Robustness and Correctness健壮性与正确性

健壮性:系统在不正常输入或不正常外部环境下仍能够表现正常。

而且即使因为意外终止执行了,也要向用户展示准确的错误信息。

面向健壮性的编程要求封闭实现细节,以达到限定用户的恶意行为的目的,并且要考虑到各种各样大的极端情况,假设用户可以做任何事情。

目的是让用户变得更容易:出错也可以容忍,因为程序内部已有容错机制。

对自己的代码要保守,对用户的行为要开放

正确性:程序按照spec加以执行的能力,是最重要的质量指标!

面向正确性的编程要求永不给用户错误的结果

目的是让开发者变得更容易:用户输入错误,直接结束。(不满足precondition的调用)

正确性倾向于直接报错(error),健壮性则倾向于容错(fault-tolerance)

提升健壮性和正确性的一般步骤:

Step 0:开发assertions, defensive programing, code review, formal validation
Step 1:发现错误Memory dump, stack traces, execution logs, testing
Step 2:错误定位bug localization, debug)
Step 3:修复code revision
6.2 Error and Exception Handling
提高健壮性:错误与异常处理

1. Java中的错误和异常


Java中错误和异常的派生关系图:


Error:与代码无关,程序员通常无能为力,一旦发生,想办法让程序优雅的结束。也可以通过对外部环境的配置解决问题,如用户输入错误、设备错误、物理限制等。

Exception:一定是程序导致的问题,可以捕获、可以处理。

由于程序员对Error通常无法预料无法解决,因此重点关注可被解决的Exception

2. 异常


异常:程序执行中的非正常事件,程序无法再按预想的流程执行。

程序除了return正常退出之外还可以通过throws抛出异常来非正常退出。

在上一部分中的图中可以看到,Java中Exception可以被分为两个部分,蓝色的运行时异常和绿色的其他异常。

运行时异常:由程序员在代码里处理不当造成,在源代码中引入了故障,而如果在代码中提前进行验证,这些故障就可以避免。动态类型检查的时候会发现这种异常,而一旦出现,代码就必然有错误,可以通过调试解决。
其他异常:由外部原因造成,程序员无法完全控制的外在问题所导致的,即使在代码中提前加以验证,也无法完全避免失效发生。
Checked and unchecked exceptions

选取checked exception还是unchecked exception可遵循下面的原则:

checked exception:如果客户端可以通过其他的方法恢复异常,而对开发者来说错误可预料但不可预防,它的出现已经脱离了程序能够掌控的范围。
unchecked exception:如果客户端对出现的这种异常无能为力,而对开发者来说错误可预料可预防,它可以通过调整程序来避免出现。
抛出异常
使用throw关键字抛出异常,如:throw new EOFException();

在抛出异常的时候利用Exception的构造函数,将发生错误的现场信息充分的传递给client。

大部分情况下,JDK所提供的异常类都足够我们使用,如果JDK提供的exception类无法充分描述程序发生的错误,可以创建自己的异常类。

定义一个checked exception


然后就可以像使用JDK提供的异常类一样使用了。

当然也可以自定义Unchecked exception,但并不推荐这么做

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


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

Unchecked exceptions也可以使用throws声明或try-catch进行捕获,但大多数时候是不需要的,也不应该这么做——掩耳盗铃,对发现的编程错误充耳不闻。因为Unchecked exceptions的目的就是为了发现错误,从而消除潜在的bug。

Checked exception应该让客户端从中得到丰富的信息。而要想让代码更加易读,倾向于用unchecked exception来处理程序中的错误

异常的再抛出:当捕获到的异常类型不希望让客户端看到的时候,就可以捕获它,然后重新抛出一个新的类型的异常。但是这么做的时候最好保留“根原因”。如:


finally
当异常抛出时,方法中正常执行的代码被终止,如果异常发生前曾申请过某些资源,那么异常发生后这些资源要被恰当的清理(不清理也行,Java提供了垃圾回收机制,只不过浪费点性能)。

所以形成了try-catch-finally结构。不管程序是否碰到异常,finally都会被执行。

详见此处

TWR:Try-with-Resources语法可以自动关闭资源,而免去了最后finally关闭的麻烦。如:


LSP中的异常


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

一张图总结


 Assertions and Defensive Programming提高正确性:断言与防御式编程

1. 断言
断言是为了在开发阶段检验某些应该成立的条件在此时是否成立,如果成立则表明程序正常,如果不成立则表明存在错误。断言即是对代码中程序员所做假设的文档化,在实际使用的时候assertion都会被disabled,就不会影响性能了。

不带信息的断言:assert condition;

带信息的断言:assert condition : message;

使用的地方:

内部不变量:判断某个局部变量应该满足的条件
表示不变量:常用的checkRep()
控制流不变量:如:不想让程序走向switch-case的某个分支,则可以用断言直接在分支上assert false;
方法的前置条件:判断传入参数是否满足前置条件,以fail fast
方法的后置条件:判断结果时候满足后置条件,程序一定要执行正确。
注意:

不要把正常的功能语句和断言在一起,如assert list.remove(x);这样在禁掉断言的时候会把功能语句一并禁掉。
程序之外的事,不受控制,不要乱断言,只有涉及到内部执行逻辑的时候才使用Assert
Assertion vs. Exception

错误/异常处理是提高健壮性,处理外部行为;断言是提高正确性,处理内部行为

使用异常来处理你“预料到可以发生”的不正常情况;使用断言处理“绝不应该发生”的情况

2. 防御式编程
对来自外部的数据源要仔细检查,例如:文件、网络数据、用户输入等

对每个函数的输入参数合法性要做仔细检查,并决定如何处理非法输入

6.4 Debugging
代码调试

1. Bug和Debugging
debug的一般过程:重现–>诊断–>修复–>反思

Debug占用了大量的开发时间,其中错误定位又占据了绝大部分的调试时间

2.诊断定位
诊断策略:

instrumentation 测量:也就是最基本的打印内容,把要观察的对象全部打印到控制台,当然,最后要把这些语句都删掉。另外,也可使用log功能统一管理。
Divide and Conquer 分治:也就是二分法,通过把代码一次次的分割,确定bug的位置。传说中的防狼围栏算法。
Slicing 切片:出错的应该是一个变量之类的东西,所以按照与这个变量对整个程序的语句做分类,只保留与该变量有关的与语句,所以大大见减小了错误定位的范围。
Focus on difference 寻找差异:
充分利用版本控制系统,找出在哪个commit之后出现了bug症状。有可能不是上个版本引入的bug,而是之前的某个版本引进的,所以需要回推到该个版本。git bisect指令可以实现这个比较定位功能,它的实现也是通过二分法定位到引入bug的版本。
Delta Debugging 基于差异的调试:通过比较各个测试用例所覆盖的语句的差异,看看未通过的测试用例与通过的测试用例的覆盖的语句多/少了哪些语句,那么这些语句就有可能是bug所在位置。
查找其他方面的差异:软硬件环境、JVM参数配置、输入文件、…
Symbolic Debugging:符号化执行,不需输入特定的值,使用“符号值”(而非“实际值”)作为输入,解释器模拟程序执行,获得每个变量的“符号化表达式”,从而可判断是否执行正确。
Debugger 调试器:单步执行发现错误。
3. Logging
日志管理工具:

JDK logging
Apache Log4j
Apache Commons-logging
SLF4J
syslog-ng
java.util.logging
通过设定日志级别来确定要log哪些信息,也可以通过Handler将日志存储在不同的地方。

日志级别(从高到低):SEVERE、WARNING、INFO、CONFIG、FINE、FINER、FINEST

设定级别:logger.setLevel(Level.INFO);

Logger

可以设定全局的logger,但是会造成信息混乱的后果,于是需要定义自己的logger,然后在程序中使用它。


Logging Handlers

Handler是日志的输出位置,缺省输出到控制台。

日志处理器也需要设定日志级别。

Handler类型:StreamHandler、ConsoleHandler、FileHandler、SocketHandler、MemoryHandler…

设定处理器:logger.addHandler(new FileHandler(“test.txt”));

日志的格式:SimpleFormatter、SimpleFormatter、…

设定格式:fileHandler.setFormatter(new SimpleFormatter());

6.5 Testing and Test-First Programming
软件测试与测试优先的编程

软件测试是提高软件质量的重要手段,目的是为了确认软件是否达到可用级别。

测试分为:单元测试、集成测试、系统测试、验收测试、回归测试

测试可以说明软件是正确的,但不能说明软件没有错误。因为很明显不可能使用穷举法进行暴力,所以测试无法覆盖到所有的可能,所以就有可能有某个未覆盖到的特例可能造成bug。

1. 测试优先编程
先写spec,再写符合spec的测试用例,最后写代码。

Test-driven development (TDD) 测试驱动的开发

使用JUnit进行单元测试
单元测试:针对软件的最小单元模型开展测试,隔离各个模块,容易定位错误和调试

JUnit在测试方法前使用@Testannotation来表明这是一个JUnit测试方法。如果要在测试开始之前做一些准备则在准备方法前添加@Beforeannotation,如果要在测试结束后做一些收尾工作则在收尾方法前添加@Afterannotation。

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

详细使用说明见 https://github.com/junit-team/junit4/wiki/Assertions

建立的JUnit测试类要和被测试类保持相同的包结构,但存放在不同的文件夹下,而在IDE中将两个文件夹都设置为源文件夹,这样在逻辑上它们目录下的同名文件夹被IDE认为是同一个包。

2. 黑盒测试
黑盒测试 (Black-box Testing):对程序外部表现出来的行为的测试。用于检查代码的功能,不关心内部实现细节。

黑盒测试的测试用例是按照spec来设计的,所以我们为了用尽可能少的测试用例,尽快运行,并尽可能大的发现程序的错误,提出如下两种按照spec设计测试用例的设计思想。

等价类划分
基于等价类划分的测试:将被测函数的输入域针对每个输入数据需要满足的约束条件划分为等价类,从等价类中导出测试用例。

这种思想基于假设:相似的输入产生相似的行为。因此我们可以从等价类中选取一个代表作为测试用例。

例如:对于一个输入数据为整数的情况来说,我们可以按照正负来划分,也可以按照奇偶来划分。

边界值分析
经验发现大量的错误发生在输入域的“边界”而非中央,所以我们要在等价类和划分的基础上增加边界值分析的方法来作为补充。

而对于有多个参数的的例子来说,不同的参数有不同的等价类划分,也有不同的边界取值,因此一个问题就是如何组合两个等价类的取值。

第一种办法是取笛卡尔积:全覆盖。将所有维度上的所有取值都组合一次,每个组合都设计一个测试用例。当然,也会存在没有意义的组合。
第二种办法是覆盖每个取值:最少1次即可。显然笛卡尔积将会产生大量的测试用例,所以这种办法是每个维度上的每个值只需要有一个测试用例覆盖即可。我们为此付出的代价是有可能降低了测试覆盖度。
3. 白盒测试
白盒测试 (White-box Testing):对程序内部代码结构的测试。要考虑内部实现细节。

白盒测试的测试用例是根据程序执行的路径来设计的,同样为了用尽可能少的测试用例,尽快运行,并尽可能大的发现程序的错误,必须根据程序具体的执行过程来设计测试用例。

独立/基本路径测试:对程序所有执行路径进行等价类划分,找出有代表性的最简单的路径(例如循环只需执行1次),设计测试用例使每一条基本路径被至少覆盖1次。

覆盖度
黑盒测试也有覆盖度,但是由于我们不知道内部实现,因此覆盖度对黑盒测试的意义不大。所以覆盖度的真正描述的是白盒测试的充分程度。

代码覆盖度越低,测试越不充分,但要做到很高的代码覆盖度,需要更多的测试用例,测试代价高

评判代码覆盖度的几个方面:

函数覆盖:所有的方法是否都被测试一次
语句覆盖:所有的语句都被测试一次
分支覆盖:所有的分支都被测试一次
条件覆盖:所有的条件的真假组合都被执行一次
路径覆盖:所有的路径都被执行一次,这是测试中最强的覆盖标准。
测试效果:路径覆盖>分支覆盖>语句覆盖

测试难度:路径覆盖>分支覆盖>语句覆盖

可以看出,测试效果越好,测试难度越大,所以我们要根据实际情况的需求,选取不同的侧重点,达到该条件下最好的测试效果。

覆盖度计算工具:EclEmma https://www.eclemma.org/

4. 自动测试和回归测试
自动测试:由于手工测试的代价太高,因此有了自动测试(自动调用被测函数、自动判定测试结果、自动计算覆盖度),只是测试用例的自动执行,并非自动生成测试用例。

在线自动化测试工具:Travis-CI https://travis-ci.org

回归测试:一旦程序被修改,重新执行之前的所有测试。因为没有办法保证修改后的程序没有引入原来没有的bug,因此即使修改前已经通过了的测试用例也需要再次进行测试。这也是为什么我们需要自动测试。

5. 记录测试策略
在测试类中要做好对等价类划分等测试策略的记录,测试策略(根据什么来选择测试用例)非常重要,需要在程序中以注释的形式记录下来。

目的:在代码评审过程中,其他人可以理解你的测试,并评判你的测试是否足够充分。
 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值