4.2.1 单元测试和功能测试
先讲讲单元测试和功能测试的差异。
单元测试:
- 目的是为了提高程序员的生产率。
- 它是高度局部化的东西,每个测试类都隶属于单一包
- 它能够测试其他包的接口,除此之外它将假设其他包一切正常。
功能测试:
- 目的是为了保证软件能够正常运作。
- 从客户的角度保障质量,并不关心程序员的生产力。
- 它们应该由一个专门的找BUG团队去负责开发,这个团队应该使用重量级工具和技术来帮助开发良好的功能测试。
当功能测试中出现问题时,要除去它至少做两件事。
1、修改代码。
2、编写单元测试,用以单独检测此BUG。
通常情况下,先编写单元测试有利于锁定BUG的位置。
我们现在所讲的JUnit框架是专门用来编写单元测试的,而功能测试往往以其他工具辅助进行。
重构需要单元测试。
4.3 添加更多测试
测试应该是一种风险驱动的行为,测试的目的是希望找到现在或将来可能出现的错误。
测试的要诀:测试你最担心出错的部分,这样收益最大。
现在我们要继续制作FileReaderTester的测试。
FileReader类什么地方容易出错呢?
4.3.1 末尾读取
其中的一个地方是:当输入流到达文件尾端,read()应该返回-1。
想想看,如果这个出错了,那就意味着我们在读取文件时会一直一直循环下去,或者报错。这不是我们期望的,所以要进行测试。
通过文本编辑器我查看到我的data.txt文件共有148个字符:
Bradman 99.94 52 80 10 6996 334 29
Pollock 60.97 23 41 4 2256 274 7
Headley 60.83 22 40 4 2256 270* 10
Sutcliffe 60.73 54 84 9 4555 194 16
所以我编写了如下代码:
/**
* 测试读取到最后一个字符后,返回值是否是-1
*
* @author newre
* @throws IOException
*/
public void testReadEnd() throws IOException {
int length = 148;
for (int i = 0; i < length; i++)
input.read();
assertEquals(-1, input.read());
}
然后添加到测试套件中:
public static Test suite() {
TestSuite suite = new TestSuite();
suite.addTest(new FileReaderTester("testRead"));
suite.addTest(new FileReaderTester("testReadEnd"));
return suite;
}
当我们执行时,会分别测试testRead()和testReadEnd()两个测试方法。
事实上,测试套件具有一个特别的构造函数,它的参数是Test子类实例对象,然后它会将这个对象所在类中,所有以“test”开头的方法都当作测试用例:
public static Test suite() {
TestSuite suite = new TestSuite(FileReaderTester.class);
return suite;
}
我没有看JUnit4这一部分的写法,可能是以注解@Test代替这种写法,会将这个类中所有具有此注解的方法当作测试用例。
4.3.2 边界查看
测试的技巧:寻找边界条件。对于文件读取而言,边界条件是第一个字符、最后一个字符、读完后结束的读取,所以我们编写了如下代码:
/**
* 读取测试文件的边界测试
*
* @author newre
* @throws IOException
*/
public void testReadBoundaries() throws IOException {
assertEquals("read first char", 'B', input.read());
int length = 148;
for (int i = 1; i < length - 2; i++)
input.read();
assertEquals("read last char", '6', input.read());
assertEquals("read at end", -1, input.read());
}
这一部分代码可能会有坑,我修改了一些内容,与书上的不太一样。
4.3.3 空白文件读取
寻找边界条件也包括寻找特殊的、可能导致失败的情况。
空文件是不错的边界条件。
/**
* 测试空文件的读取
*
* @author newre
* @throws IOException
*/
public void testEmptyRead() throws IOException {
File empty = new File("src/main/resources/empty.txt");
FileOutputStream out = new FileOutputStream(empty);
out.close();
FileReader in = new FileReader(empty);
assertEquals(-1, in.read());
}
此处的FileOutputStream的运用其实不是很好,因为它的作用仅仅是为了在没有empty的时候创建它,防止找不到文件的异常。
使用File的empty.exists(); empty.createNewFile()
可能会好一点?
现在,我们等于又有了一个空文件的实例化,这一部分也同样可以放在setUp()中。
由于空文件的建立是一组代码,所以我们可以把那一部分代码提炼出来。
private File empty;
protected void setUp() throws Exception {
try {
input = new FileReader("src/main/resources/data.txt");
empty = newEmptyFile();
} catch (FileNotFoundException e) {
throw new RuntimeException("unable to open test file.");
}
}
private File newEmptyFile() throws IOException {
File empty = new File("src/main/resources/empty.txt");
if(!empty.exists())
empty.createNewFile();
return empty;
}
/**
* 测试空文件的读取
*
* @author newre
* @throws IOException
*/
public void testEmptyRead() throws IOException {
FileReader in = new FileReader(empty);
assertEquals(-1, in.read());
in.close();
}
4.3.4 末尾的末尾读取
如果读取文件末尾之后的位置,会发生什么事?同样应该返回-1,在testReadBoundaries()方法中添加语句:
assertEquals("read pass end", -1, input.read());
测试时,需要积极地去寻找,怎么才能破坏代码,制造BUG。
在写好测试代码后,需要去测试这部分代码是否真的能测试出错误。
4.3.5 尾声
虽然说,“任何测试都不能证明一个程序没有BUG”,但这并不影响我们的测试——测试总能够暴露一些问题,也能提高编程速度。
如果是专门搞测试的,也许需要考虑到所有所有所有的情况,但我们是开发的,没那么多闲工夫把所有情况考虑上,我们只能凭借程序员的直觉,测试一些很容易出错的地方。
当测试类增多,就能够体现出组合模式的好处:测试类符合树状的组织结构,都是同一类型的,具有同意行为的类,所以我们可以专门制作一个主控的测试类:
public class MasterTester extends TestCase {
public static void main(String[] args) {
junit.textui.TestRunner.run(suite());
}
public static Test suite() {
TestSuite suite = new TestSuite();
suite.addTest(new TestSuite(FileReaderTester.class));
return suite;
}
}
说了这么多,其实要点只有一个:请构筑一个良好的BUG检测器并经常运行它,这对任何开发工作都将大有裨益,并且是重构的前提。