软件构造
软件构造的多维度视图和质量目标
多维度视图
-
三个维度:
-
按阶段划分: 构造时/运行时视图
-
按动态性划分: 时刻/阶段视图
-
按构造对象的层次划分:代码/构件视图
-
-
五个关键性指标
-
可理解性
-
可维护性
-
可复用性
-
健壮性
-
高性能
-
软件系统的构成:软件 = 模块(组件) + 数据/控制流
-
质量目标
-
外部质量指标:影响用户的体验
-
外部质量取决于内部质量
-
-
内部质量指标:影响软件本身和它的开发者
外部质量
-
正确性:按照预先定义的
规约
执行-
最重要的质量标准
-
测试和调试:发现不正确、消除不正确
-
防御式编程:在写程序时就确保正确性
-
形式化方法(用的较少):通过形式化验证发现问题
-
-
-
健壮性:针对异常情况的处理,出现异常时不要”崩溃“
-
健壮性是对正确性的补充
-
正确性:软件的行为要严格符合规约中定义的行为
-
健壮性:出现规约定义之外情况的时候,软件要做出恰当的反应
-
-
异常情况依据规约判定,未被specification覆盖的情况为
异常情况
-
所谓
异常
,取决于spec的范畴
-
-
可扩展性:根据软件的规约的变化调整软件系统产品
-
软件规模越大,扩展起来越不容易
-
规模变大不可避免,需要在增加软件规模的同时保证软件系统的可扩展性
-
-
可扩展性主要是为了应对变化
-
简约主义设计(ADT and OOP)
-
分离主义设计(高内聚、低耦合,Modularity and adaptability)
-
-
-
可复用性:一次开发,多次使用
-
发现共性
-
不重复自己,不重复造轮子
-
-
兼容性:不同的软件系统之间相互可容易的集成
-
保持设计的同构性(设计标准,标准化)
-
文件格式的标准化:在Unix系统中,text file 就是简单的字符序列
-
数据结构的标准化:在Lisp系统中,所有数据、程序都是用二叉树来表示的
-
用户接口的标准化:不同版本的Window,所有工具依赖于和用户进行交流的图像化界面,基于标准组件如窗口、图标等。
-
-
难点:不同软件有不同的设定/规定
-
文件格式的兼容性(Linux和Windows的可执行文件的格式)
-
-
-
高性能:处理器时间、内存空间占用、外存空间占用、带宽占用
-
保证足够的正确性追求性能
-
对性能的关注需要与其他质量属性进行折中
-
过度的优化导致软件不再适应变化和复用
-
算法
-
I/O处理、内存管理
-
-
-
可移植性:软件可方便的在不同的软件环境之间移植
-
跨硬件、跨操作系统
-
-
易用性:容易学、安装、操作、监控
-
给用户提供详细的指南
-
结构简单
-
理解用户,做好充分调研,了解用户的使用习惯
-
-
功能性:软件系统提供的功能丰富与否
-
程序设计中一种不适宜的趋势,即软件开发者增加越来越多的功能,企图跟上竞争,其结果是程序极为复杂、不灵活、占用过多的磁盘空间。
-
每增加一小点功能,都确保其他质量属性不受到损失。
-
-
-
及时性:软件系统需要在用户想要之前及时发布
-
好的idea需要及时的转换成软件系统,快速占据市场
-
-
其他质量指标
-
可验证性
-
完整性
-
可修复性
-
经济性
-
内部质量
-
源代码相关因素:代码行数(Lines of Code, LOC)、循环复杂度等
-
体系结构相关因素:耦合、内聚等等
-
代码可读性
-
好理解性
-
整洁性
-
规模
质量指标折中
-
正确的软件开发过程中,开发者应该将不同质量因素之间如何做出折中的设计决策和标准明确写下来
-
虽然需要折中,但“正确性”绝不能与其他质量因素折中
-
最重要的质量因素:
-
正确性和健壮性(可靠性)
-
可扩展性和可复用性(模块化)
-
OOP技术提高质量
-
正确性:封装、去中心化
-
健壮性:封装、异常处理
-
可扩展性:封装、信息隐藏
-
可复用性:模块化、组件、模型、模式
-
兼容性:标准模块、接口
-
可移植性:信息隐藏、抽象
-
易用性:GUI组件、框架
-
高性能:重用组件
-
及时性:建模、重用
-
经济性:重用
-
功能性:可扩展性
软件构造的五个关键质量目标
-
易于理解:可理解性
-
应对变化:可维护性和适应性
-
更低的开发成本:可复用性
-
更少的bug:健壮性
-
更好的性能:高性能
软件测试与测试优先的编程
-
测试是确保程序正确性/健壮性的最普遍的手段
-
设计测试用例
-
设计必须仔细和系统
-
-
用相关的工具(JUnit)写测试程序
-
自动化测试过程
-
-
测试的困难点:
-
穷举+暴力 是不可能的
-
靠偶然测试没意义
-
基于样本的统计数据对软件测试意义不大
-
软件与物理产品的巨大差异
-
硬盘抽样检测其损坏率等统计测试
-
-
-
软件行为在离散输入空间中差异巨大
-
大多数的正确不代表软件正确性高
-
bug的出现往往不符合特定概率分布
-
-
无统计分布概率可循
-
-
软件测试
-
软件测试是提高软件质量的重要手段
-
执行一系列的测试用例,发现bugs,确认是否达到可用级别(用户需求)
-
关注系统的某个侧面的质量特性
-
即使是最好的测试,也无法达到100%的无错误
-
残留缺陷率:1000行代码有多少个bug(defects/kloc)
-
1- 10 defects/kloc : 典型行业软件
-
0.1-1defects/kloc:高质量验证,java类库
-
0.01-0.1 defects/kloc: 最好的、安全的验证,NASA和医疗器械软件等。
-
-
测试特点
-
测试与其他活动的目标相反:破坏、证错、“负能量”
-
再好的测试也无法证明系统中没有错误
-
好的测试:
-
能发现错误
-
不冗余
-
最佳特性,多种测试方案中最好的
-
别太复杂也别太简单
-
测试级别
-
单元测试:测试最低、最小的模型和模块(方法和类)
-
集成测试:测试类与类之间、接口与接口之间是否能正常工作
-
系统测试:测试整个系统是否达到了设计目标(开发公司自己做的测试)
-
验收测试:用户按照自己的测试标准对系统进行测试
-
回归测试:为了验证更高级别的测试不会对系统引入新的bug,重新测试低层次的测试
静态测试和动态测试
-
静态测试:不需要运行程序(靠眼睛看,靠工具进行静态分析)
-
静态测试通常是隐含的,如校对,以及当编程工具/文本编辑器检查源代码结构或编译器(预编译器)检查语法和数据流时作为静态程序分析。
-
-
动态测试:使用给定的一组测试用例执行编程的代码
-
动态测试可以在程序 100% 完成之前开始,以便测试特定的代码部分并应用于离散的功能或模块。
-
典型的技术是使用存根/驱动模块或从调试器环境执行。
-
测试和调试
-
测试:发现是否存在错误(先测试,看看自己写的是否全覆盖)
-
调试:识别错误根源,消除错误
白盒测试和黑盒测试
-
白盒测试:对程序内部代码结构的测试
-
根据代码结构设计测试用例
-
-
黑盒测试:对程序外部表现出来的行为的测试
-
不关心代码实现或者不知道代码实现,根据规约设计测试用例
-
测试用例
-
测试用例:输入+执行条件+期望结果
-
测试用例是为特定目标设计,例如执行特定的程序路径或验证是否符合特定要求
-
E.g., test cases: {2,4}, {0,0}, {-2,4} for program y=x^2
-
先写测试用例 -> 组织测试用例 -> 执行测试用例 -> 获得结果并报告
单元测试
-
针对软件的最小单元模型(比如类)开展测试,隔离各个模块,容易定位错误和调试
单元测试考虑情况
-
接口模块:测试输入输出
-
本地数据结构:大的算法有很多步骤,看步骤与步骤之间的数据是否是一致的,测试数据一致性
-
所有独立路径:测试模块中所有的语句是否有被执行到
-
边界值情况:测试模块是否合理地处理了边界条件
-
异常路径:测试所有的异常处理路径
单元测试过程
-
因为一个组件不是独立的,每个单元测试必须要提供驱动模块和桩模块
-
驱动模块:是一个接受测试用例,传递这些测试用例给测试组件并打印相关结果的前驱模块
-
桩模块:被测试模块调用的且未开发完的模块
-
为测试某个“模块”建立一个模块可以运行的“环境“
-
-
自动化单元测试JUnit
-
JUnit是Java中广泛应用的单元测试框架
-
运行时自动判断测试是否通过
JUnit测试案例
-
在每个测试方法前面加上
@Test
标注指明-
一些测试并检查结果给出断言的方法:
assertEqual、 assertTrue、assertFalse
-
-
第一个参数为期望结果、第二个参数为实际结果
-
测试方法各自不影响,一个方法的结果测试失败不会影响其他方法
黑盒测试
-
黑盒测试:用于检查代码的功能,不关心内部实现细节
-
黑盒测试尝试发现以下类型的错误
-
功能不正确或者有缺失
-
接口错误
-
数据结构错误或者外部数据库访问错误
-
行为错误
-
初始化和终止错误
-
-
黑盒测试的测试用例
-
检查程序是否符合规约(在正确的输入下产生正确的输出则通过)
-
用尽可能少的测试用例,尽快运行,并尽可能大的发现程序的错误
-
等价类划分
-
基于等价类划分的测试:将被测函数的输入域划分为等价类,从等价类中导出测试用例
-
假设等价类的所有值的测试效果一致,因此每个等价类只需要选出一个测试用例(降低测试用例)
-
等价类:一组对象间存在对称、传递和自反的关系
-
每个等价类代表着对输入约束加以满足/违反的有效/无效数据的集合
-
开根号,用一个负数等价类表示无效数据的合理处理
-
-
-
划分基本准测(划分必须是完备的,每一个划分加在一起必须是输入空间,不过不同的划分之间不需要满足什么关系)
-
输入数据限定了数值范围:
-
eg: [0,10],则划分为三个等价类:一个有效类[0,10],两个无效类(-00,0)和(10,+00)
-
-
输入数据指明了特定的值:
-
划分为两个等价类:一个为指定的值,另一个为其他的值
-
-
输入数据确定了一组数值:
-
划分为两个等价类:一个为集合内部的值组成的等价类,另一个为集合外部的值组成的等价类
-
-
边界值分析
-
大量的错误发生在输入域的
边界值
而非中央 -
边界值分析方法是对等价类划分方法的补充
-
在等价类划分时,将边界作为等价类之一加入考虑
-
设计测试用例
-
笛卡尔积:全覆盖
-
多个划分维度上的多个取值,组合起来,每个组合都有一个用例
-
上述max:共3 x 5 x 5 = 75 test cases(但不是所有组合都可以)
-
a = 0 ,b = 0 ,a = b 这种组合就不行
-
-
-
-
覆盖每个取值:最少一次即可
-
每个维度的每个取值至少被一个测试用例覆盖一次即可
-
### 白盒测试
-
白盒测试需要考虑内部实现细节
-
如根据输入选择不同的算法实现,因此划分等价类需要根据这些区域
-
如具体的实现保持一个内部缓存保存先前输入的结果,因此我们需要测试重复的输入,看是否被cache了
-
-
独立/基本路径测试:对程序所有执行路径进行等价类划分,找出有代表性的最简单的路径(例如循环只需执行1次),设计测试用例使得每一条基本路径被至少覆盖一次。(分支、循环边界处)
代码覆盖度
-
代码覆盖度:已有的测试用例有多大程度覆盖了被测程序
-
代码覆盖度越低,测试越不充分。(越高需要更多的测试用例,测试代价高)
-
从低到高的覆盖:
-
函数覆盖
-
语句覆盖
-
分支覆盖
-
条件覆盖
-
路径覆盖
-
-
测试效果:路径覆盖 > 分支覆盖 > 语句覆盖(难度关系也是一致的)
-
路径覆盖:由于路径数量巨大,难以覆盖
-
测试优先的编程
-
A 为测试优先的编程
-
测试优先的编程:在写代码之前先写测试
-
测试代码
比写代码更有成就感 -
当有大量的代码文件时,在最后写测试往往使得debug更久且更加痛苦,因为bugs在代码的各个地方
-
先做测试会节省大量的调试时间
-
-
-
过程
-
先写spec
-
再写符合spec的测试用例
-
写代码、执行测试、有问题再改、再执行测试用例直到通过它
-
-
写测试用例就是理解、修正、完善spec设计的过程
-
规约
-
给定参数类型和附加约束
-
返回值类型和返回值如何依赖于输入
-
规约由注释(对输入和输出的描述)和方法签名构成
-
-
-
测试优先有助于更好地理解规约
-
规约本身也可能是错误的(不正确、不完整、模棱两可的、缺少边界的情况
-
测试优先可以尽早地发现设计问题,避免浪费时间做错误的事情
-
测试策略文档化
-
测试策略需要在程序中显示记录下来
-
代码评审时,其他人可以理解测试并评判测试是否充分
-
-
记录方法:
-
在整个测试类的最前面记录下测试策略
-
在每个测试方法前注释好测试哪一个等价类
-