51Testing丛书连载:(三) 互联网单元测试及实践

发表于:2008-7-08 17:47

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:陈卫俊 赵璨 周磊等    来源:51Testing软件测试网

5.4  方法的性能测试

  单元性能测试的概念、工具等都已经介绍完了。接下来就针对单元测试阶段所能发现的各种性能问题,详细说明如何通过单元性能测试,来发现这些性能问题,即怎么做单元性能测试。在本章中这部分内容侧重于阐述测试方法,在第6章和第7章中,将分别给出一个完整的实例,从实践的角度进一步来说明,以帮助读者更好地掌握这些内容。

  首先来看看方法的性能测试。执行方法的性能测试,主要的目的有3个。

  1.通过测试找出大量耗时的方法,并对其进行优化。无论是单次执行方法耗时很多,还是由于方法被调用次数过多而引起大量耗时,最终的结果都是程序执行的时间很长,对于用户来说就是程序的响应速度慢。在单元测试阶段即找出明显耗时多的方法并优化它们,可以有效地避免在系统性能测试阶段花费很大的精力来查找程序到底慢在哪里。

  2.通过测试保证代码在重构后性能没有下降。无论是出于什么原因需要重构代码,通常其执行时间都不应该比重构之前更慢。因此,可以设定一个基准时间(基准时间可以参考重构前代码执行的时间,或者参考系统的相关性能要求等),在每次重构后立即运行测试用例,当测试用例执行时间超过基准时间的时候,即认为该测试用例未通过,然后根据测试的结果优化重构后的代码,直到测试用例通过为止。运行测试用例应该是一个自动化的过程,开发人员可以很容易地反复运行这些测试用例,通过这样的测试,重构后的代码的性能得到了很好的保障。

  3.通过测试找出最适合所开发系统的第三方组件。根据所开发系统的业务特点(数量级、输入实例的初始状态等),设计测试场景,然后在测试场景下对备选的第三方组件进行性能测试,通过分析测试结果,可以很轻松地找到最合适的第三方组件。

5.4.1  分析方法的执行时间

  先来看一段代码。

代码5.12  最简单的I/O

01import java.io.*;

02

03public class IOReader1 {

04

05  public static void main(String args[]) {

06    try {

07      FileInputStream fis = new FileInputStream(args[0]);

08      DataInputStream dis = new DataInputStream(fis);

09      int cnt = 0;

10      while (dis.readLine() != null)

11      cnt++;

12       fis.close();

13      System.out.println(cnt);

14    } catch (Throwable e) {

15      System.err.println("exception");

16    }

17  }

18

19}

  上面这段代码的功能是采用最简单的I/O方式,实现对文件的读取,最后返回文件的行数。这段代码执行时绝大部分时间用在I/O上,一旦将文件内容读取到内存以后,剩下的操作仅仅是计算文件的行数,并没有复杂的计算工作,因此剩余部分执行所用的时间很短。这点稍后通过执行时间分析可以明显看出来。那么,给出这段代码的目的,就是通过单元性能测试,分析这种最简单的I/O方式的执行时间,看看其性能如何,是否能满足大多数情况下的性能要求。然后再使用其他的I/O技术,通过单元性能测试分析看看是否能提高I/O的性能。

  另外有一点需要说明一下,细心的读者可能会发现,上面代码的第10行中,DataInputStream.readLine()这个方法是一个过时的方法,目前已经不建议使用该方法。不过为了说明问题,这里仍然使用了该方法,但不会影响到之后的分析。

  在这里将采用Eclipse TPTP进行执行时间分析。首先当然是将以上代码加入到Project中,读者可能需要稍做修改以使代码能正常运行,然后就可以开始分析执行时间了。先打开Profile窗口进行一些必要的设置,选择“Run”→“Profile…”,如图5.15所示。

 

图5.15  打开Profile窗口

  在Profile窗口中双击“Java Application”,可以看到IOReader1类被载入。选择IOReader1,为其指定输入参数,即要读取的文件,在“Arguments”中进行设置,如图5.16所示。

软件测试

图5.16  配置要读取的文件

  为了更清晰地看出方法执行时间上的差异,这里选择了一个1MB大小的Word文件。接下来需要在“Monitor”中进行设置,由于我们所关注的是方法的执行时间,因此选择“Execution Time Analysis”,如图5.17所示。

软件测试

图5.17  设置Monitor

  设置完成后单击“Apply”,再单击“Profile”就开始进行方法执行时间分析了。执行过程可能需要等上十几秒,完成后在“Console”中将输出所选择的文件的行数。现在来看看该方法的执行时间分析。双击“Execution Time Analysis”,打开“Execution Stastics”窗口,如图5.18所示。

软件测试

图5.18  打开Execution Stastics窗口

  从Execution Stastics窗口可以看到,只执行了一个main方法,该方法被调用一次,耗时8.887秒。这个结果显然没有什么问题,因为从代码中可以看到,该类确实只有一个main方法。但是可以看到,该方法仅仅读取一个1M大小的文件就要耗时近9秒,这样的性能显然差强人意,大多数情况下,该方法不能满足系统的性能要求。目前的结果信息对于方法的性能分析来说也显得不足,无法从中看出为什么main方法要执行这么长的时间。那么就需要对此进一步进行分析。

  要进一步分析main方法的执行时间,就需要把main方法所调用的类的执行时间信息都收集起来进行分析。因此,需要打开“Execution Time Analysis”的Options进行设置,勾选“Collect boundary classes excluded by the filter set”选项,深度(depth)一般设为3就可以了,如图5.19所示。

软件测试

图5.19  设置Execution Time Analysis的Options

  再次执行时间分析,这次可能需要等更长的时间,完成后可以看到如图5.20所示的结果。

软件测试

图5.20  IOReader1执行时间详细分析

  很好,现在信息足够多了,可以来看看为什么main执行得这么慢了。上图显示的是方法按累积执行时间降序排列的结果,所谓累积执行时间,即方法自身执行时间以及该方法所调用的其他方法执行时间的总和。所以main方法理所当然的排在第一位,因为main方法是位于最顶层的方法,它的累积执行时间就是全部的执行时间。排在第二位的就是前面提到的DataInputStream.readLine(),这是一个底层的系统级方法。

  问题就在这里了,由于main方法触发了底层运行时系统调用DataInputStream. readLine(),而这个方法的I/O性能不佳,因此main方法执行很耗时。那么显然如果要提升性能的话,DataInputStream.readLine()是不能再使用了,必须采用其他的方法。可以进一步分析一下为什么DataInputStream.readLine()性能不佳。双击上图中的readLine()方法这一行,可以打开“Method Invocation Details”窗口,在里面显示了该方法调用和被调用的详细信息,如图5.21所示。

软件测试

图5.21  readLine的详细执行时间分析

  可以看到,DataInputStream. readLine()总共调用了6个方法,其中有一个read int()方法的累积执行时间占了绝大部分(图中圈出的行),继续双击read int()方法进一步分析,得到结果如图5.22所示。

  现在结果已经很明确了,因为从上图中可以看到,read int()又调用了一个read int()方法,而被调用的read int()方法执行了5.1秒,因此全部执行时间的50%以上是被它占用了,这是由于这种方式下I/O是一个字节一个字节地读取,因此性能不佳。

软件测试

图5.22  read int()的详细执行时间分析

  以上就完成了方法的执行时间分析。当运行单元测试后发现执行时间过长时,就可以采用这样的分析方法定位问题,然后针对问题予以解决。当然,在系统性能测试时如果发现程序性能不佳时也可以这么来分析,Eclipse TPTP可以通过使用Agent Controller(另一个开源软件)做代理来对远程服务器上的应用进行执行时间分析,但是在系统性能测试的时候,影响性能的因素非常之多,各种因素对于问题的定位干扰很大,因此,强烈建议在单元测试时就关注方法的执行时间,并对执行时间过长的方法进行分析,定位并解决问题。这样做要比在后期才测试并发现问题,然后解决的成本低得多。

  继续回到IOReader1上来。现在通过执行时间分析,发现IOReader1不能满足性能要求,而引起性能问题的原因是底层的系统级方法I/O性能不佳,因此需要采用新的方法来解决这个性能问题。可以采用大缓冲区来解决这个问题,看看下面代码。

代码5.13  使用大缓冲区的I/O

01import java.io.*;

02

03public class IOReader2 {

04

05    public static void main(String args[]) {

06        try {

07            FileReader fr = new FileReader(args[0]);

08            BufferedReader br = new BufferedReader(fr);

09            int cnt = 0;

10            while (br.readLine() != null)

11             cnt++;

12            fr.close();

13            System.out.println(cnt);

14        } catch (Throwable e) {

15            System.err.println("exception");

16        }

17    }

18

19}


  注意第8行,这里使用了BufferedReader类,这是一个支持缓冲的类,先从磁盘读取大块文件,然后每次读取一个字符(BufferedInputStream类与之相似,但是它每次读取一个字节)。第10行的BufferedReader.readLine()方法从输入缓冲区获取下一个字符,而不是从底层系统,这样一来仅仅在最开始读取大块文件时,访问了一次底层系统,避免了大量的底层运行时系统调用。

  接下来对IOReader2类进行执行时间分析。跟分析IOReader1一样的做法,可以得到如图5.23所示的结果。

软件测试

图5.23  IOReader2的执行时间分析

  可以看到,main方法耗时3.99秒,这意味着IOReader2比IOReader1在执行时间上要快了55%以上,即性能提升了一倍多。

  进一步分析,时间主要消耗在fill() void方法上,双击该行打开方法详细信息窗口,如图5.24所示。

软件测试

图5.24  fill() void方法的详细信息

  从图中显示的结果来看,似乎fill() void方法没有调用其他的方法,其自身执行用时3.89秒,不过这是一个假象,实际上并不一定是fill() void方法执行了这么长时间,它可能调用了其他执行比较慢的方法。还记得之前对“Execution Time Analysis”的Options做的设置吗?当时调用深度(Depth)设置为3,而fill() void方法就是调用深度为3的方法,其调用关系为,main方法调用readLine()方法,readLine()方法调用readLine(boolean)方法,readLine(boolean)方法调用fill() void方法,那么fill() void方法调用了什么方法?因为没有收集更深层的调用信息,所以从这次的结果是看不到fill() void方法调用了什么方法的。不过这个没有关系,因为fill() void方法是一个底层的系统级方法,不需要去关心它调用了什么方法或者它是如何实现的,如果性能不能满足要求,那么优化的手段就是换别的方法,而不用去重构fill() void方法来优化性能。

  现在初步的性能优化工作已经完成了,I/O的性能提升了一倍多,在一般情况下可能已经可以满足性能要求了。不过有些情况下可能有更高的性能要求,I/O可能得更快一些才行,那就需要进一步的优化。

  这次将直接缓冲,而不再使用IOReader2里的BufferedReader对象,这样可以排除readLine方法的调用,代码如下。

代码5.14  直接缓冲的I/O

01import java.io.*;

02

03public class IOReader3 {

04  public static void main(String args[]) {

05    if (args.length != 1) {

06      System.err.println("missing filename");

07      System.exit(1);

08    }

09    try {

10      FileInputStream fis =

11          new FileInputStream(args[0]);

12      byte buf[] = new byte[2048];

13      int cnt = 0;

14      int n;

15      while ((n = fis.read(buf)) != -1) {

16        for (int i = 0; i < n; i++) {

17          if (buf[i] == '\n')

18            cnt++;

19        }

20      }

21      fis.close();

22      System.out.println(cnt);

23    }

24    catch (IOException e) {

25      System.err.println(e);

26    }

27  }

28}

  看看IOReader3的执行时间分析结果,如图5.25所示。

  从图上看,结果很惊人,main方法只执行了不到0.1秒,这是因为IOReader3里面没有readLine方法,而从前面对IOReader1和IOReader2的执行时间分析可以看出,readLine方法占了绝大部分的执行时间,所以当IOReader3避免readLine方法后,执行速度得到了惊人的提升。

软件测试

图5.25  IOReader3的执行时间分析

  看起来现在已经很完美了,IOReader3的性能肯定能满足要求了。不过这里有一个问题必须要说,IOReader3的自己做缓冲的方式虽然性能远胜于IOReader2,但并不表示总是应该使用IOReader3。这可能是一个错误的倾向,特别是在处理文件结束事件时没有仔细的实现,使得IOReader3在一些输入条件下可能在功能上是错误的。比如这个例子中,读取一个1MB的Word文件并返回该文件的行数,这种情况下IOReader3的输出结果就是错误的。

  就如前面已经说过的那样,一个功能不正确的方法,其性能再好也是没用的,质量合格的单元必须同时通过了单元功能测试和单元性能测试,两者之间存在着平衡,绝不是执行越快就越好。另外,在可读性上,IOReader3也没有其他方法好,这会给代码的维护带来麻烦。因此,在这个例子里面,IOReader2才是对大多数应用最适合的。除非系统很特殊,对I/O有非常高的要求,这时候才考虑使用IOReader3的方式。

连载一 连载二

本文选自:《51Testing软件测试作品系列》之三的互联网单元测试及实践 ,本站经电子工业出版社和作者的授权,近期将进行部分章节的连载,敬请期待!

版权声明:51Testing软件测试网及相关内容提供者拥有51testing.com内容的全部版权,未经明确的书面许可,任何人或单位不得对本网站内容复制、转载或进行镜像。51testing软件测试网欢迎与业内同行进行有益的合作和交流,如果有任何有关内容方面的合作事宜,请联系我们。

《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号