# 第4章:构筑测试体系
# 一、自测试代码的价值
确保所有测试都完全自动化,让它们检查自己的测试结果。
一套测试就是一个强大的bug侦测器,能够大大缩减查找bug所需要的时间。
Java
之中的测试关勇收发是 testing main
,意思是每个类都应该有一个用于测试的 main()
。这是一个合理的习惯(尽管并不那么值得称许),但可能不好操控。这种做法的问题是很难轻松运行多个测试。另一种做法是:建立一个独立类用于测试,并在一个框架中运行它们,使测试工作更轻松。
# 二、JUnit测试框架
首先准备一个数据文件 data.txt
:
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
2
3
4
下面是我创建的测试类:
public class FileReaderTester extends TestCase {
private FileReader _input;
public FileReaderTester(String name) {
super(name);
}
@Override
protected void setUp() throws Exception {
try {
_input = new FileReader("data.txt");
} catch (FileNotFoundException e) {
throw new RuntimeException("unable to open test file");
}
}
@Override
protected void tearDown() {
try {
_input.close();
} catch (IOException e) {
throw new RuntimeException("error on closing test file");
}
}
public void testRead() throws IOException {
char ch = '&';
for (int i = 0; i < 4; i++) {
ch = (char) _input.read();
}
assertEquals('e', ch);
}
public static Test suite() {
TestSuite suite = new TestSuite();
suite.addTest(new FileReaderTester("testRead"));
return suite;
}
public static void main(String[] args) {
TestRunner.run(suite());
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
其中 setUp()
和 tearDown()
是覆盖父类 TestCase
的方法,前者用来测试前的初始化操作,后者是测试后的回收工作。
testRead()
是我们写的需要测试的方法,suite()
中利用反射将 FileReaderTester
类的 testRead()
方法加入测试,最终运行 main()
函数进行测试:
.
Time: 0.001
OK (1 test)
2
3
4
1 test
表示运行了1个测试,OK
表示测试通过。
如果修改测试访问故意不对:
public void testRead() throws IOException {
char ch = '&';
for (int i = 0; i < 4; i++) {
ch = (char) _input.read();
}
assertEquals('e', ch);
}
2
3
4
5
6
7
在此运行:
.F
Time: 0.002
There was 1 failure:
1) testRead(com.jerry.refactoring.ch2.FileReaderTester)junit.framework.AssertionFailedError: expected:<e> but was:<d>
at com.jerry.refactoring.ch2.FileReaderTester.testRead(FileReaderTester.java:50)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at com.jerry.refactoring.ch2.FileReaderTester.main(FileReaderTester.java:60)
FAILURES!!!
Tests run: 1, Failures: 1, Errors: 0
2
3
4
5
6
7
8
9
10
11
12
Failures: 1
表示出现了1处错误。
频繁的运行测试,每次编译请把测试也考虑进去——每天至少执行每个测试一次
如果我们在测试方法中提前关闭流:
public void testRead() throws IOException {
char ch = '&';
_input.close();
for (int i = 0; i < 4; i++) {
ch = (char) _input.read();
}
assertEquals('d', ch);
}
2
3
4
5
6
7
8
在此运行,也会报错:
.E
Time: 0.001
There was 1 error:
1) testRead(com.jerry.refactoring.ch2.FileReaderTester)java.io.IOException: Stream closed
at sun.nio.cs.StreamDecoder.ensureOpen(StreamDecoder.java:46)
at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:148)
at sun.nio.cs.StreamDecoder.read0(StreamDecoder.java:127)
at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:112)
at java.io.InputStreamReader.read(InputStreamReader.java:168)
at com.jerry.refactoring.ch2.FileReaderTester.testRead(FileReaderTester.java:49)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at com.jerry.refactoring.ch2.FileReaderTester.main(FileReaderTester.java:61)
FAILURES!!!
Tests run: 1, Failures: 0, Errors: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
注意这次就是 Errors: 1
了。
# 单元测试和功能测试
单元测试是高度局部化的东西,每个测试类都隶属于单一包。它能够测试其他包的接口,除此之外 它将架设其他包一切正常。
功能测试就完全不同。它们用来保证软件能够正常运作。它们从客户的角度保障质量,并不关心程序员的生产力。
一般而言,功能测试尽可能地把整个系统当做一个黑箱。
一旦找到软件中的 bug
,至少要做两件事。除了要修改代码才能排除错误,还应该添加一个单元测试,用来暴露这个 bug
。
每当你收到bug报告,清先写一个单元测试来暴露bug。
# 三、添加更多测试
编写未臻完善的测试病实际运行,好过对碗没测试的无尽等待。
因为前面的 data.txt
一共有 225 个字符,所以写下面这个测试方法:
public void testReadAtEnd() throws IOException {
int ch = -1234;
for (int i = 0; i < 225; i++) {
ch = _input.read();
}
assertEquals(-1, _input.read());
}
2
3
4
5
6
7
还需要在 suite
中添加刚才的方法:
public static Test suite() {
TestSuite suite = new TestSuite();
suite.addTest(new FileReaderTester("testRead"));
suite.addTest(new FileReaderTester("testReadAtEnd"));
return suite;
}
2
3
4
5
6
不过这样每次新增方法都需要对 suite
进行 addTest()
操作有些麻烦,可以直接使用 TestSuite
的一个用 class
作为参数的构造方法,这样会将 class
对应类中所有的以 test
开头的函数进行测试,也就不需要写 suite()
方法了。
public static void main(String[] args) {
TestRunner.run(new TestSuite(FileReaderTester.class));
}
2
3
执行结果:
```console
..
Time: 0.002
OK (2 tests)
2
3
4
5
再添加一个测试边界的方法:
```java
public void testReadBoundaries() throws IOException {
assertEquals("read first char", 'B', _input.read());
int ch ;
for (int i = 0; i < 223; i++) {
ch = _input.read();
}
assertEquals("read last char", '6', _input.read());
assertEquals("read at end", -1, _input.read());
}
2
3
4
5
6
7
8
9
10
11
12
13
还是使用上面 TestSuite
方式的测试,直接运行:
...
Time: 0.002
OK (3 tests)
2
3
4
考虑可能出错的边界条件,把测试火力集中在那儿。
对于文件相关测试,空文件是个不错的边界条件,在 setUp()
方法中加点东西:
private FileReader _empty;
@Override
protected void setUp() throws Exception {
try {
_input = new FileReader("data.txt");
_empty = newEmptyFile();
} catch (FileNotFoundException e) {
throw new RuntimeException("unable to open test file");
}
}
private FileReader newEmptyFile() throws IOException {
File empty = new File("empty.txt");
FileOutputStream out = new FileOutputStream(empty);
out.close();
return new FileReader(empty);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
使用 _empty
记录一个空文件的 FileReader
。
测试空文件的方法:
public void testEmptyRead() throws IOException {
File empty = new File("empty.txt");
FileOutputStream out = new FileOutputStream(empty);
out.close();
FileReader in = new FileReader(empty);
assertEquals(-1, in.read());
}
2
3
4
5
6
7
运行测试:
....
Time: 0.004
OK (4 tests)
2
3
4
如果读取文件末尾之后的位置,会发生什么事?同样应该返回 -1。修改之前测试边界的方法:
public void testReadBoundaries() throws IOException {
assertEquals("read first char", 'B', _input.read());
int ch;
for (int i = 0; i < 223; i++) {
ch = _input.read();
}
assertEquals("read last char", '6', _input.read());
assertEquals("read at end", -1, _input.read());
assertEquals("read past end", -1, _input.read());
}
2
3
4
5
6
7
8
9
10
测试结果:
....
Time: 0.003
OK (4 tests)
2
3
4
尝试在关闭流后再读取它,应该得到一个 IOException
异常,这样子测试:
public void testReadAfterClose() throws IOException {
_input.close();
try {
_input.read();
fail("no exception for read past end");
} catch (IOException io) {
}
}
2
3
4
5
6
7
8
这么写,当发生 IOException
之外的任何异常都将以一般方式形成一个错误。
测试结果:
.....
Time: 0.005
OK (5 tests)
2
3
4
当事情被认为会出错时,别忘了检查是否抛出了语气的异常。
随着测试类愈来愈多,你可以生成另一个类,专门用来包含由其他测试类所组成的测试套件。这样,你就可以拥有一个“主控的”测试类:
public class MasterTester extends TestCase {
public static void main(String[] args) {
TestRunner.run(suite());
}
private static Test suite() {
TestSuite result = new TestSuite();
result.addTest(new TestSuite(FileReaderTester.class));
return result;
}
}
2
3
4
5
6
7
8
9
10
11
12
如果有新的测试类需要加进来只需要使用 addTest()
添加到 result
中就可以了。
不要因为测试无法捕捉所有bug就不写测试,因为测试的确可以捕捉到大多数bug。
← 第3章:代码的坏味道 第5章:重构列表 →