一、健壮性(Robustness)和正确性(Correctness)
健壮性
系统在不正常输入或不正常外部环境下仍能够表现正常的程度。
面向健壮性的编程:处理未期望的行为和错误终止,即使终止执行,也要准确且无歧义地向用户展示全面的错误信息。
程序员需要考虑到任何可能出现的错误操作,总是假定自己的代码可能失败,考虑的方面越多,程序的健壮性就越高。
“对别人宽容点,对自己狠一点。”
正确性
程序按照spec加以执行的能力,这是最重要的质量指标。
两者的区别
正确性是永远给出正确的答案;健壮性是尽可能保持软件正常运行而不是异常退出。
正确性倾向于直接报错(error);健壮性倾向于容错(fault-rolerance)。
正确性让开发者变得更容易,用户输入错误则直接结束;健壮性让用户变得更容易,出错也可以容忍。
对外的借口倾向于健壮;对内的借口倾向于正确。
可靠性 = 健壮性 + 正确性
二、错误和异常处理
内部错误
例如内存溢出、栈溢出等。
程序员通常无能为力,一旦出现内部错误,想办法让程序优雅地结束。
异常
程序执行中的非正常事件,使程序无法再按照预想的流程进行。
异常是自己程序导致的问题,可以捕获并处理。
异常的种类
运行时异常(RuntimeException):由程序员在代码里处理不当造成,在代码中提前验证就可以避免运行时异常。
其他异常:由程序员无法控制的外部原因造成,即使在代码中提前验证也无法完全避免。
检查异常(Checked Exception)和未检查异常(Unchecked Exception)
Checked异常:是Exception的子类型,必须捕获并指定错误处理器handler,否则编译无法通过,类似于静态类型检查。
处理方法:声明“本方法可能会发生XXX异常”,抛出XXX异常,使用try-catch-finally捕获并处理XXX异常。
Unchecked异常:是RuntimeException的子类型,可以不处理并通过编译,但执行时出现就会导致程序失败,类似于动态类型检查。
处理方法:可以抛出异常,也可以使用try-catch-finally捕获并处理,但不需要也不应该这么做。
应该采用哪种异常?
如果客户端可以通过其他的方法恢复异常,就采用checked异常;如果客户端对出现的这种异常无能为力,就采用unchecked异常。
异常出现的时候,要做一些试图恢复它的动作而不要仅仅打印它的消息。
用throws声明checked异常
class Person {
public method() throws FileNotFoundException {
......
}
}抛出异常
throw new FileNotFoundException();或
FileNotFoundException e - new FileNotFoundException();
throw e;class Person {
public method() throws FileNotFoundException {
if(...) { throw new FileNotFoundExceotion(); }
}
}自定义异常
如果JDK提供的Exception类无法充分描述程序发生的错误,可以创建自己的异常类。
class PersonException extends Exception {
public PersonException() { super(); }
public PersonException(String message) { super(message); }
public PersonException(String message, Throwable cause) { super(message, cause); }
public PersonException(Throwable cause) { super(cause); }
}捕捉异常
使用try-catch来捕捉异常(当需要捕捉异常时,Eclipse会提醒程序员用try-catch包围该段代码)
try {
code
} catch (ExceptionType e) {
handler for this type
}当异常抛出时,前面正常执行的代码被终止,如果异常发生前曾申请过某些资源,那么异常发生后这些资源要被恰当地清理,这就需要用到finally块。
InputStream in = new FileInputStream(...);
try {
code
} catch (ExceptionType e) {
handler for this type
} finally {
in.close();
}三、断言与防御性编程
断言
在开发阶段的代码中嵌入,检验某些“假设”是否成立。若成立,表明运行正常,否则表明存在错误。
如果出现AssertionError,意味着内部某些假设被违反了。
何时使用断言
断言主要用来判断内部不变量、表示不变量、控制流不变量、方法的前置条件、方法的后置条件等是否满足。
使用断言的主要目的是为了在开发阶段调试程序,尽快避免错误,在运行阶段不再使用断言,因为断言非常影响运行时的性能。
使用异常来处理“预料到可以发生”的不正常情况,使用断言处理“绝不应该发生”的情况。
防御性编程
对来自外部的数据源要仔细检查,对每个函数的输入参数合法要做仔细检查,并决定如何处理非法输入。
隔离舱:从类的public方法接收到的外部数据都要处理过再传递到private方法。
四、调试
Bug种类
Math bugs:例如0做被除数。
Logic bugs:例如无限循环。
Syntax bugs:例如操作符错误。
Resource bugs:例如使用未初始化的变量。
Team working bugs:例如说明和产品不符合。
Debug
debug是测试的后续步骤,测试发现问题,debug消除问题。
“症状”和“原因”可能相隔很远,这是高耦合导致的结果。修改一个bug可能会同时改正其他bug。
步骤:复现bug → 诊断bug → 修复bug → 确定bug不会再次出现
Debug的方法
Memory Dump:在程序运行完成后,通过dump来分析出现的错误。
Stack Trace:stack trace是在执行程序的特定点上所有挂起的方法调用的列表。
Printf debugging:在程序内部各部分展示程序执行时的动态信息,比使用静态dump分析更有效。
Logging:日志。通过设定日志级别来确定要log哪些信息,log结果可被多种渠道加以处理,可通过设定条件进行过滤,并输出为多种格式。可使用层次化的多个日志记录器。
Complier Warning Messages:把编译器的warning level调到最高级别,消除所有warning。
Breakpoints:在代码中设置断点,来检查程序运行到这里的情况。
五、测试与测试优先编程
测试
白盒测试:对程序内部代码结构的测试。
黑盒测试:对程序外部表现出来的行为的测试。
测试思想:我的程序永远是“错”的,对自己的程序“狠”一点。
测试用例
test case = {test inputs + execution conditions + expected results} 输入+执行条件+期望结果
测试用例的书写规则:最可能发现错误,不重复、不冗余、最有效、既不简单也不复杂。
测试优先编程
先写spec,再写符合spec的测试用例。
先写测试会节省大量的调试时间。
单元测试
针对软件的最小单元模型开展测试,隔离各个模块,容易定位错误和调试。
可以使用JUnit来进行单元测试。
assertArrayEquals("failure - byte arrays not same", expected, actual);
assertEquals("failure - strings are not equal", "text", "text");
assertFalse("failure - should be false", false);
assertNotNull("should not be null", new Object());
assertNotSame("should not be same Object", new Object(), new Object());
assertNull("should be null", null);
assertSame("should be same", aNumber, aNumber);
assertTrue("failure - should be ture", true);
assertThat("good", allOf(equalTo("good"), startsWith("good")));
代码覆盖度
测试效果:路径覆盖>分支覆盖>语句覆盖
测试难度:路径覆盖>分支覆盖>语句覆盖
本文详细探讨了软件的健壮性和正确性,解释了两者的区别。接着介绍了错误和异常处理,包括内部错误、运行时异常、检查异常和未检查异常的处理方式。此外,还讲解了防御性编程和断言的重要性,以及调试方法和测试优先编程,以增强软件的健壮性。
869

被折叠的 条评论
为什么被折叠?



