HIT 软件构造 9 :第2,12讲

目录

一、软件测试与测试优先的编程

1. 黑盒测试用例的设计(2.6)

(1)等价类划分(2.6.1)

(2)边界值分析(2.6.2)

2. 以注释的形式撰写测试策略(2.10)

3. JUnit测试用例写法(2.5)

4. 测试覆盖度(2.8)

二、面向正确性与健壮性的软件构造

1. 健壮性和正确性(12.1)

2. Throwable(12.3)

3. Error/Runtime异常、其他异常

4. Checked异常、Unchecked异常(12.4.3)

5. Checked异常的处理机制

6. 自定义异常类(12.4.6)

7. 断言的作用、应用场合(12.5)

8. 防御式编程的基本思路(12.6)


第2,12讲的考点如下:

一、软件测试与测试优先的编程

1. 黑盒测试用例的设计(2.6)

黑盒测试:对程序外部表现出来的行为的测试

黑盒测试:用于检查代码的功能,不关心内部实现细节

黑盒测试试图找出以下类型的错误:

–功能不正确或缺失

–接口错误

–数据结构或外部数据库访问错误

–行为或表现错误

–初始化和终止错误

检查程序是否符合规约,用尽可能少的测试用例,尽快运行,并尽可能大的发现程序的错误。

(1)等价类划分(2.6.1)

基于等价类划分的测试:将被测函数的输域划分为等价类, 从等价类中导出测试用例。

针对每个输入数据需要满足的约束条件,划分等价类。每个等价类代表着对输入约束加以满足/违反的有效/无效数据的集合。

基于的假设:相似的输入,将会展示相似的行为。故可从每个等价类中选一个代表作为测试用例即可,从而可以降低测试用例数量。

–如果输入条件指定了一个范围,则定义一个有效的等价类和两个无效的等价类。

–如果输入条件需要特定值,则定义一个有效和一个无效的等价类。

–如果输入条件指定集合的一个成员,则定义一个有效和一个无效的等价类。

–如果输入条件是布尔型的,则定义一个有效类和一个无效类。

(2)边界值分析(2.6.2)

大量的错误发生在输入域的“边界 ”而非中央。边界值分析方法(BVA)是对等价类划分方法的补充。

在等价类划分时,将边界作为等价类之一加入考虑。

覆盖的两个极端:

1. 完全笛卡尔积:全覆盖,测试完备,但用例数量多,测试代价高

分区尺寸的每个合法组合都包含在一个测试用例中。对于包含边界的最大示例,它有三个维度,分别为3个部分、5个部分和5个部分,这意味着最多3×5×5=75个测试用例。实际上并非所有这些组合都是可能的。例如,无法覆盖组合a<b,a=0,b=0,因为a不能同时小于零和等于零。

2. 覆盖每个部分:覆盖每个取值:最少1次即可,测试用例少,代价低,但测试覆盖度未必高。

每个维度的每个部分至少包含一个测试用例,但不一定包含每个组合。如果仔细选择,max的测试套件可能只有5个测试用例。

2. 以注释的形式撰写测试策略(2.10)

测试策略(根据什么来选择测试用例)非常重要,需要在程序中显式记录下来

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

3. JUnit测试用例写法(2.5)

流行的单元测试框架:JUnit

JUnit是广泛采用的Java单元测试框架。

–JUnit在测试驱动开发的开发中非常重要,是单元测试框架家族中的一员,统称为xUnit。

JUnit在编译时作为JAR链接;对于JUnit3.8及更早版本,该框架位于包junit.framework下;对于JUnit4及更高版本,该框架位于包org.junit下。

Junit测试用例

JUnit单元测试是以注释@test前面的方法编写的。单元测试方法通常包含对被测试模块的一个或多个调用,然后使用assertEquals、assertTrue和assertFalse等断言方法检查结果。

4. 测试覆盖度(2.8)

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

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

覆盖的种类:函数覆盖、语句覆盖、分支覆盖、条件覆盖、路径覆盖

测试效果:路径覆盖>分支覆盖>语句覆盖

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

二、面向正确性与健壮性的软件构造

1. 健壮性和正确性(12.1)

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

面向健壮性的编程:处理未期望的行为和错误终止,即使终止执行,也 要准确/无歧义的向用户展示全面的错误信息。错误信息有助于进行debug。

总是假定用户恶意、假定自己的代码可能失败,把用户想象成白痴,可能输入任何东西。封闭实现细节,限定用户的恶意行为。考虑极端情况,没有“不可能”。

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

正确性:永不给用户错误的结果    健壮性:尽可能保持软件运行而不是总是退出

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

2. Throwable(12.3)

所有异常对象的基类是java.lang.Throwable,以及它的两个子类java.lang.Exception和java.lang.Error。

在Java编程语言中,异常对象总是从Throwable派生的类的实例。

3. Error/Runtime异常、其他异常

1. Error类(12.3)

描述Java运行时系统内部很少发生的系统错误和资源耗尽情况(例如VirtualMachineError、LinkageError)。

注意:不应该抛出这种类型的对象。

内部错误:程序员通常无能为力,一旦发生,想办法让程序优雅的结束

错误的种类:用户输入错误,设备错误,物理限制

一些典型错误:

(1)VirtualMachineError:抛出表示Java虚拟机已损坏或已耗尽继续运行所需的资源。

-OutOfMemoryError:当Java虚拟机由于内存不足而无法分配对象,并且垃圾收集器无法提供更多内存时抛出。

-StackOverflowerError:由于应用程序递归太深而发生堆栈溢出时抛出。

-InternalError:抛出以指示JVM中发生了一些意外的内部错误。

(2)LinkageError:一个类对另一个类有某种依赖性;然而,后一类在前一类的编译之后发生了不可比拟的变化。

-NoClassDefFoundError:如果JVM或类加载器实例试图加载类的定义,但找不到定义,则引发

2. Exception类(12.4.1)

描述由程序引起的错误(例如FileNotFoundException、IOException)。

这些错误可以由您的程序捕获和处理(例如,执行替代操作或通过关闭所有文件、网络和数据库连接来优雅地退出)。

异常:程序执行中的非正常事件,程序无法再按预想的流程执行。将错误信息传递给上层调用者,并报告“案发现场”的信息。是return之外的第二种退出途径。

方法抛出一个封装错误信息的对象。方法立即退出,不返回任何值。此外,在调用该方法的代码处不会恢复执行;相反,异常处理机制开始搜索可以处理此特定错误条件的异常处理程序,若找不到异常处理程序,整个系统完全退出。

3. Runtime异常(12.4.2)

运行时异常:由程序员在代码里处理不当造成

从RuntimeException继承的异常包括以下问题:

-杂乱排绕

–越界数组访问

–空指针访问

运行时异常,是程序源代码中引入的故障所造成的,如果在代码中提前进行验证,这些故障就可以避免

4. 其他异常(12.4.2)

其他异常:由外部原因造成

–试图读取超过文件结尾的内容

–试图打开一个不存在的文件

–尝试为不表示现有类的字符串查找类对象

非运行时异常,是程序员无法完全控制的外在问题所导致的。即使在代码中提前加以验证(文件是否存在),也无法完全避免失效发生。

5. 总结

4. Checked异常、Unchecked异常(12.4.3)

(1)Checked异常:

必须捕获并处理异常,或者通过声明方法抛出异常来告诉编译器您无法处理它,然后使用方法的代码将必须处理该异常(如果无法处理该异常,则可以选择声明它抛出该异常)。编译器将检查我们是否完成了两件事情之一(catch或declare)。

必须捕获并指定错误处理器handler,否则编译无法通过。类似于编程语言中的static type checking

五个关键字  – try – catch – finally – throws – throw

throws:声明“本方法可能会发生XX异常

throw:抛出XX异常

try、catch、finally:捕获并处理XX异常

异常也可以使用throws声明或try/catch进行捕获,但大多数时候是不需要的,也不应该这么做——掩耳盗铃, 对发现的编程错误充耳不闻

(2)Unchecked异常:编译器不检查错误和运行时异常

错误表示在应用程序之外发生的情况,例如系统崩溃。运行时异常通常由应用程序逻辑中的错误引起。在这种情况下,不能做任何事情,但必须重新编写程序代码。所以这些不是由编译器检查的。这些运行时异常将在开发和测试期间发现。然后我们必须重构代码以消除这些错误。

Unchecked异常:(Error和Runtime异常)不需要在编译的时候用try…catch等机制处理。

注:可以不处理,编译没问题,但执行时出现就导致程序失败,代表程序中的潜在bug。类似于编程语言中的 dynamic type checking

(3)总结

1. 如果客户端可以通过其他的方法恢复异常,那么采用checked exception;

如果客户端对出现的这种异常无能为力,那么采用unchecked exception;

异常出现的时候,要做一些试图恢复它的动作而不要仅仅的打印它的信息。

2. 尽量使用unchecked exception来处理编程错误,如果client端对某种异常无能为力,可以把它转变 为一个unchecked exception,程序被挂起并返回客户端异常信息。

Checked exception应该让客户端从中得到丰富的信息。 错误可预料,但无法预防,但可以有手段从中恢复,此时使用checked exception。

要想让代码更加易读,倾向于用unchecked exception来处理程序中的错误。可预料但不可预防,脱离了你的程序的控制范围

3. 对特殊结果(即预期情况)使用Checked exception,使用unchecked exception来发出错误信号(意外故障)

应该仅使用unchecked exception来表示意外故障(即bug),或者如果您希望客户机通常会编写代码来确保异常不会发生,因为有一种方便且廉价的方法来避免异常;否则应使用checked exception。

5. Checked异常的处理机制

(1)声明:throws抛出异常(12.4.4)

“异常”也是方法和client端之间spec的一部分,在post-condition中刻画。标头更改以反映方法可以抛出的checked异常

public FileInputStream(String name) throws FileNotFoundException

表示特殊结果的检查异常总是用Javadoc@throws子句记录,指定特殊结果发生的条件。Java还可以使用throws声明,要求在方法签名中包含异常。程序员必须在方法的spec中明确写清本方法会抛出的所有checked exception, 以便于调用该方法的client加以处理。

一个方法也可以抛出多个异常。Error 和 unchecked exceptions无法抛出。

抛出异常包括:1. 你所调用的其他函数抛出了一个checked exception——从其他函数传来的异常 2. 当前方法检测到错误并使用throws抛出了一个checked exception——你自己造出的异常

此时需要告知你的client需要处理这些异常,如果没有handler来处理被抛出的checked exception,程序就终止执行。

如果子类型中override了父类型中的函数,那么子类型中方法抛出的异常不能比父类型抛出的异常类型更宽泛。子类型方法可以抛出更具体的异 常,也可以不抛出任何异常。如果父类型的方法未抛出 异常,那么子类型的方法也不能抛出异常。

(2)抛出:throw异常(12.4.5)

利用Exception的构造函数,将发生错误的现场信息充分的传递给client。

throw异常的方法:

找到一个能表达错误的Exception类/或者构造一个新的Exception类,构造Exception类的实例,将错误信息写入,抛出它。一旦抛出异常,方法不会再将控制权返回给调用它的client,因此也无需考虑返回错误代码。

(3)捕获与处理(12.4.7、12.4.8)

异常发生后,如果找不到处理器, 就终止执行程序,在控制台打印出stack trace。

也可以不在本方法内处理, 而是传递给调用方,由client处理。

编译器严格执行throws说明符。如果调用抛出选中异常的方法,则必须处理或传递它。

尽量在自己这里处理,实在不行就往上传——要承担责任!但有些时候自己不知道如何处理,那么提醒上家,由client自己处理。

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

(4)清理现场、释放资源(12.4.9)

当代码抛出异常时,它将停止处理方法中的剩余代码并退出该方法。如果该方法获取了一些只有该方法知道的资源(文件、数据库连接等),并且该资源必须被清除,则这是一个问题。如果异常发生前曾申请过某些资源,那么异常发生后这些资源要被恰当的清理。

一种解决方案是捕获并重新引用所有异常。但是这个解决方案很乏味,因为您需要清理正常代码和异常代码中两个位置的资源分配。

使用finally解决此问题。

Try-Catch-Finally:无论是否捕获到异常,finally子句中的代码都会执行。

三种情况:

1. 不抛出异常:

2. 抛出异常被捕获:

3. 抛出异常,但异常不被捕获:

可以使用finally子句而不使用catch子句。

InputStream in = . . .;
try {
    code that might throw exceptions
}
finally {
    in.close();
}

return之后仍然运行finally。

6. 自定义异常类(12.4.6)

如果JDK提供的exception类无法充分描述你的程序发生的错误,可以创建自己的异常类

要定义checked异常,创建java.lang.exception的子类(或子类的层次结构)

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); }
}

有时会出现不希望强制每个方法在其throws子句中声明异常实现的情况。在这种情况下,您可以创建一个扩展java.lang.RuntimeException的unchecked异常。方法可以抛出或传播fooRuntimeException异常而不声明它。

public class FooRuntimeException extends RuntimeException {
    ...
}

public void calculate(int i) {
    if (i < 0) {
        throw new FooRuntimeException("i < 0: " + i);
    }
}

7. 断言的作用应用场合(12.5)

最好的防御:不要引入bug

如果无法避免,尝试着将bug限制在最小的范围内,限定在一个方法内部,不扩散。尽快失败,就容易发现、越早修复

断言:当不满足前提条件时,此代码通过抛出AssertionError异常终止程序。

检查前置条件是防御式编程的一种典型形式

–真正的程序很少没有bug。

–防御性编程提供了一种减轻bug影响的方法,即使不知道它们在哪里。

断言:在开发阶段的代码中嵌入,检验某些“假设”是否 成立。若成立,表明程序运行正常,否则表明存在错误。

每个断言都包含一个布尔表达式,该表达式在程序执行时为真。

如果不是真的,JVM将抛出AssertionError。此错误表示您有一个需要修复的无效假设。增强程序员对代码质量的信心:对代码所做的假设都保持正确。断言即是对代码中程序员所做假设的文档化,也不会影响运行时性能。

使用场景:

1. 内部不变量: 断言某个值在某个约束内,例如,断言x>0。

2. 表示不变量: 断言对象的状态在约束内。在方法执行之前或之后,类的每个实例必须是什么样的?类不变量通常通过私有布尔方法进行验证,例如checkRep()。

3. 控制流不变量: 断言不会到达某个位置。例如,switch case语句的default子句。

4. 方法的前置条件: 调用方法时什么必须为真?通常用方法的参数或对象的状态来表示。

5. 方法的后置条件: 在一个方法成功完成后,什么是真的?

6. 仅输入变量的值不会被方法更改

7. 指针不为空

8. 传入方法的数组或其他容器可以包含至少X个数据元素

9. 表已初始化为包含实值

10. 当方法开始执行(或完成)时,容器为空(或满)

11. 高度优化、复杂方法的结果与较慢但编写清晰的例程的结果相匹配

断言主要用于开发阶段,避免引入和帮助发现bug。实际运行阶段, 不再使用断言,避免降低性能。

注意:程序之外的事,不受你控制,不要乱断言

比如:文件/网络/用户输入等

断言只是检查程序的内部状态是否符合规约,断言一旦false,程序就停止执行,代码无法保证不出现此类外部错误,要使用Exception机制去处理。

许多断言机制的设计使断言仅在测试和调试期间执行,并在程序发布给用户时关闭。断言是一个很好的工具,可以保护您的代码不受bug的影响,但是Java在默认情况下关闭了断言!要记得打开(-ea)

断言通常包括程序的正确性问题。使用断言处理 “绝不应该发生”的情况

–如果针对异常情况触发断言,纠正措施不仅仅是优雅地处理错误,而是更改程序的源代码、重新编译并发布新版本的软件。

Exception通常包括程序的健壮性问题。使用异常来处理你“预料到可以发生”的不正常情况

–如果错误处理代码用于处理异常情况,则错误处理将使程序能够优雅地响应错误。

8. 防御式编程的基本思路(12.6)

防御式编程是一种防御性设计,旨在确保软件在不可预见的情况下继续运行。在需要高可用性、安全性或安全性的情况下,通常使用防御编程实践。

防御式编程技术:

1. 保护程序不受无效输入的影响 2. 断言 3. 异常 4. 特定错误处理技术 5. 路障 6. 调试辅助工具

防御式编码的最佳形式不是首先插入错误,可以将防御编程与其他技术结合使用。

保护程序不受无效输入的影响:对来自外部的数据源要仔细检查,例如:文件、网络数据、用户输入等;对每个函数的输入参数合法性要做仔细检查,并决定如何处理非法输入

设置路障:

1. 类的public方法接收到的外部数据都应被认为是dirty的,需要处理干净再传递到private方法——隔离舱。“隔离舱”外部的函数应使用异常处理,“隔离舱”内的函数应使用 断言。

2. 操作间技术。数据在进入操作间之前要消毒。操作间里的任何东西都是安全的。关键的设计决策是决定在错左键放什么,不放什么,门放在哪里,哪些例行程序被认为在安全区内,哪些在外面,哪些对数据进行了消毒。最简单的方法通常是在外部数据到达时对其进行消毒,但数据通常需要在多个级别进行消毒,因此有时需要进行多个级别的消毒。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值