# 第4章:构筑测试体系

loading

# 一、自测试代码的价值

确保所有测试都完全自动化,让它们检查自己的测试结果。

一套测试就是一个强大的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
1
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());
    }

}
1
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)
1
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);
    }
1
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
1
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);
    }
1
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
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());
    }
1
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;
    }
1
2
3
4
5
6

不过这样每次新增方法都需要对 suite 进行 addTest() 操作有些麻烦,可以直接使用 TestSuite 的一个用 class 作为参数的构造方法,这样会将 class 对应类中所有的以 test 开头的函数进行测试,也就不需要写 suite() 方法了。

    public static void main(String[] args) {
        TestRunner.run(new TestSuite(FileReaderTester.class));
    }
1
2
3

执行结果:

```console
..
Time: 0.002

OK (2 tests)
1
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());
    }
1
2
3
4
5
6
7
8
9
10
11
12
13

还是使用上面 TestSuite 方式的测试,直接运行:

...
Time: 0.002

OK (3 tests)
1
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);
    }
1
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());
    }
1
2
3
4
5
6
7

运行测试:

....
Time: 0.004

OK (4 tests)
1
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());
    }
1
2
3
4
5
6
7
8
9
10

测试结果:

....
Time: 0.003

OK (4 tests)
1
2
3
4

尝试在关闭流后再读取它,应该得到一个 IOException 异常,这样子测试:

    public void testReadAfterClose() throws IOException {
        _input.close();
        try {
            _input.read();
            fail("no exception for read past end");
        } catch (IOException io) {
        }
    }
1
2
3
4
5
6
7
8

这么写,当发生 IOException 之外的任何异常都将以一般方式形成一个错误。

测试结果:

.....
Time: 0.005

OK (5 tests)
1
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;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

如果有新的测试类需要加进来只需要使用 addTest() 添加到 result 中就可以了。

不要因为测试无法捕捉所有bug就不写测试,因为测试的确可以捕捉到大多数bug。


上次更新: 2020-08-21 09:02:51(10 小时前)