“开发控制台应用程序,怎么有效地对输出进行测试?”这是我带给TWU的coach的第一个问题,因为我那时受到这个问题的折磨。
虽然使用控制台作为UI的简陋的交互式应用程序项目已经鲜见了,但我们仍然常常会用控制台程序来设计工具或者做简单输入输出的小项目。作为控制台程序的功能测试,它结合用户输入,根据控制台的输出来判断程序结果的正确与否。当来到TDD篝火前的我们头一回写这样的测试时,心中可能会有些困惑 :
控制台又不像既接受参数又有返回值的函数,如何把我的测试数据填入控制台,之后再取出控制台输出的结果来做断言呢?
我不赞成只为准备输出数据的函数添加单元测试,虽然这是必要的,但它是不够的。就像隔靴搔痒,没有直接测到控制台输出结果。
我不赞成在测试中粘贴大段的期望的输出字符串,我们心里都清楚,那其中只有寥寥几行是真正有用的。代码丑陋,而且看不到测试的重点。
我们如何有效地进行测试?
讨论两种情况:
1、我们面对的是一个清爽的应用程序,它得到用户输入后能得出简洁的输出结果,如加法计算器接受“1+2”返回“3”;
2、我们不得不面对一个输出内容庞杂的应用程序,其中被用来做断言的只是其中只言片语,如若干段说明中的一行公式。
对于情况一
(下文以Java为例讲解)
控制台使用标准流System.out和System.in来完成输出输入,我们虽然不得不在产品代码中使用它们,但在测试中可以用易控制的替代品代替它们,以设定输入并截获输出。为此,需要在程序的开始处做抽象,以方便产品代码和测试代码指定不同的输入输出方式。
改造后的程序开始处,代码示例如下:
System.out是字节流(byte streams, 参考[1]),但它被定义为PrintStream类对象,此类组合了一个内部的字符流(character streams, 参考[1])对象以实现字符流的特性。
我们可以在程序的开始处指定负责输出的对象类型为PrintStream,再在测试代码中实例化一个PrintStream对象,并为其加入一个指定的字节流对象作为记录输出内容的载体(如ByteArrayOutputStream对象)。
System.in也是字节流,它没那么好运气,其自身不具备字符流的特性,所以需要使用InputStreamReader来包装,之后作为字符流使用。但InputStreamReader没有方便的方法按行读取输入数据,为了进一步使用readLine()方法,我们在它之外再用缓冲流(buffered streams, 参考[1])的BufferedReader类对象包装。
所以,在程序的开始处我们指定负责输入的对象类型为BufferedReader,再在测试代码中实例化一个的BufferReader类对象,为其添入含有测试输入数据的StringBuilder对象。
测试代码示例如下:
测试的输入是“5 14“,输出是“5 plus 14 is 19”加换行。
产品代码中,程序入口的示例如下:
情况一总结:我们通过从程序开始就替换输入输出流对象的方法,获得指定控制台输入对应的输出结果,完成控制台的功能测试。
情况二
如果单纯为获得程序输出的话,我们还可以采用直接重定向标准输出到文件而不是到控制台的方法。
测试可通过扫描截获的输出内容来匹配测试要求的模式,并限定超时时间,以此完成功能测试。这种功能测试方法的缺点是测试时间也许会慢,但功能测试有有多少是很快的呢?呵呵呵
当然这种方法已经不单单适用于控制台程序了。
参考
[1] 关于byte streams、character streams和buffered streams,请参考链接
http://download.oracle.com/javase/tutorial/essential/io/index.html