引言
EMMA 是一个开源、面向 Java 程序测试覆盖率收集和报告工具。它通过对编译后的 Java 字节码文件进行插装,在测试执行过程中收集覆盖率信息,并通过支持多种报表格式对覆盖率结果进行展示。 EMMA 所使用的字节码插装不仅保证 EMMA 不会给源代码带来“脏代码”,还确保 EMMA 摆脱了源代码的束缚,这一特点使 EMMA 应用于功能测试成为了可能。
注意:
- 在测试中使用 EMMA 收集覆盖率信息之前,需要从 EMMA 的网站上下载 emma.jar 包。在这个网站上还可以得到更多关于 EMMA 的资源。
- EMMA 只能收集 Java 代码的覆盖率。
文章附录提供一个样例代码,包含一个 WAR 包和一个 JAR 包,其中需要将 WAR 包安装在WebSphere Portal Server 上运行。在实际测试过程中,可以将它们替换成对应的被测对象。
功能测试中使用 EMMA 的优点
EMMA 收集的数据包括类覆盖率、方法覆盖率、块覆盖率和行覆盖率,这些数据以包为单位进行组织。
大多数功能测试中,测试人员一般不能直接得到被测源代码,源代码也不是测试人员关心的重点。在具体的测试过程中,功能测试人员一般以一个有意义的功能模块作为测试关心的重点,而能够反映一定功能含义的类和方法的覆盖率在功能测试中更有价值。因此,在功能测试中,类覆盖率和方法覆盖率是测试人员关心的重点,行和块覆盖率则作为测试的参考。
测试覆盖率报告中包含了两个方面的内容,测试覆盖的部分和未被测试覆盖的部分。尽管百分之百的测试覆盖率不能代表被测对象完全没有问题,但是测试覆盖的部分以及覆盖比率可以增加测试者对测试工作的信心,指导测试执行以及测试的方向。另一方面,当测试用例执行出现异常时,针对每个测试用例的测试报告还可以提供可疑代码的范围,为代码纠错提供帮助。
测试覆盖率报告中未覆盖的部分也同样有价值:
- 表明测试可能不完整,有些功能、代码没有被测试覆盖到。
- 为测试用例的设计提供指导建议。在覆盖率报告的指导下,测试人员有目的地与开发人员进行讨论,确定未覆盖部分是测试的空白还是不需要测试的部分。
- 帮助开发人员发现无用代码,为修改,完善代码提供依据。
在使用 EMMA 获得测试覆盖率过程中,类、方法等覆盖的百分比报告,可以方便测试人员更好的评估测试。测试人员通过对照覆盖率报告与测试用例设计文档,需求文档可以迅速找到测试的不足。通过与开发人员进行讨论,可以更好的评估测试力度,并指导进一步的测试。因此在功能测试中引入覆盖率信息,能够完善测试结果报告,确保测试质量和力度,保证测试按质、按量地完成。
特别是在目前倡导的 Agile 开发和测试流程中,开发和测试的周期都很短,有效的覆盖率信息能够帮助测试人员更加准确地控制测试结果和周期、跟踪问题,保证软件正常发布。
EMMA 在测试执行中的应用
在这一部分将逐步介绍 EMMA 在功能测试过程中的使用过程和步骤。为了使整个介绍过程容易理解,在文章附录中提供了示例程序,文章中通过对示例程序进行操作介绍使用 EMMA 的命令。
插装被测组件
EMMA 通过对被测组件进行插装来跟踪被测组件的执行过程,因此对被测组件进行插装是使用 EMMA 获得覆盖率信息的第一步。测试人员应首先和开发人员讨论,确定哪一部分包含了符合插装要求的文件( Java 文件),哪一部分需要考虑覆盖率信息,然后选择合适的方式进行插装。
- 插装准备
在执行插装操作之前,首先应该扩展 Java 虚拟机,即将 emma.jar 放到被测组件运行使用的JRE 目录下面作为 JRE 的扩展,以便 EMMA 能够被调用。 emma.jar 包含了 EMMA 核心功能模块的实现和 EMMA 运行时所需的类文件,这些文件是使用 EMMA 所必需的。
由于示例被测组件运行在 Websphere Portal Server 中,并使用默认的 JRE 运行,因此将emma.jar 放到 “/opt/WebSphere/PortalServer/java/jre/lib/ext” 下面。在实际的测试中,将该路径进行相应的替换。
- 插装
EMMA 中提供了 “instr” 命令完成插装操作。插装操作可以面向 JAR 包、 WAR 包、 WAR 包、类文件和目录,选择合适的命令进行插装可以使插装过程变得简便。下面的1-4通过具体例子介绍了不同情况下的插装命令。
- 插装目录和类文件
对于类文件,通过指定类文件所在的目录实现。
清单 1. 对类文件插装命令
/opt/WebSphere/PortalServer/java/jre/bin/java emma instr -m overwrite -ip /opt/WebSphere/PortalServer/installedApps/NumberQuizWEB_10yggsru.ear/NumberQuizWEB.war/WEB -INF/classes -Dmetadata.out.file=/root/emma/Number_coverage.em EMMA: processing instrumentation path EMMA: instrumentation path processed in 682 ms EMMA: [5 class(es) instrumented, 0 resource(s) copied] EMMA: metadata merged into [/root/emma/Number_coverage.em] {in 72 ms}
- 插装 JAR 包
JAR 包可以作为一个整体进行插装。通过对整个 JAR 进行插装,可以避免对 JAR 包进行解压和压缩的过程,提高插装效率。
清单 2. 对 JAR 包插装命令
/opt/WebSphere/PortalServer/java/jre/bin/java emma instr -m overwrite -cp TestWs.jar - Dmetadata.out.file=/root/emma/Number_coverage.em EMMA: processing instrumentation path EMMA: instrumentation path processed in 675 ms EMMA: [7 class(es) instrumented, 4 resource(s) copied] EMMA: metadata merged into [/root/emma/Number_coverage.em] {in 60 ms}
- 插装 WAR/EAR 包
由于 WAR/EAR 包需要运行在特定的环境中,所以在进行插装之前,需要先将其安装在特定的 J2EE 容器中,然后将其看作目录进行插装。
清单 3. 对 WAR/EAR 包插装命令
/opt/WebSphere/PortalServer/java/jre/bin/java emma instr -m overwrite -ip NumberQuizWEB.war -Dmetadata.out.file=/root/emma/Number_coverage.em EMMA: processing instrumentation path EMMA: instrumentation path processed in 610 ms EMMA: [5 class(es) instrumented, 0 resource(s) copied] EMMA: metadata merged into [/root/emma/Number_coverage.em] {in 94 ms}
- 选择性的插装
EMMA 支持对整个 JAR 包和目录进行插装,但如果在 JAR 包或者目录中包含系统的文件或者测试过程中不关心的文件时,应该进行选择性插装,因为这些文件的存在会影响测试结果的百分比。 EMMA 提供了选择插装的选项,实现选择性插装。
清单 4. 选择插装命令
/opt/WebSphere/PortalServer/java/jre/bin/java emma instr -m overwrite -cp TestWs.jar -ix +org.wstest.service.* -Dmetadata.out.file=/root/emma/Number_coverage.em EMMA: processing instrumentation path EMMA: instrumentation path processed in 637 ms EMMA: [4 class(es) instrumented, 6 resource(s) copied] EMMA: metadata merged into [/root/emma/Number_coverage.em] {in 107 ms}
上述命令选择了与清单2中同样的 JAR 包,由于只包含了 org.wstest.service.* 内的内容,因此只插装了4个类。
以上的1-4分别介绍了在插装过程中的常用命令,下面对命令中用到的一些参数进行解释。
参数 “m”代表插装后文件输出的模式。有三个值可供选择: “copy” ,“overwrite” 和 “fullcopy” 。其中,“copy” 和 “ fullcopy” 这两种模式将会改变插装文件所在的目录,并需要测试人员手动为其生成所需的包,使用起来比较复杂。“overwrite” 模式直接用插装后的文件覆盖插装前文件,使用方便。但是由于同一时间生成的文件只能插装一次,在 “overwrite”模式下,插装前的文件已经丢失,测试人员无法重复插装操作,因此建议在插装之前先将需要插装的文件和包进行备份。
参数 “ip” 和 “cp” 用来提供插装路径,其中 “cp” 用来指明一个文件夹, “ip” 指定单独的文件或者 JAR 包。
参数 “Dmetadata.out.file” 用来指定插装得到的元数据文件保存的路径。
EMMA 中通过 “ix” 参数指定文件的包含和排除关系,其中在 “+” 符号后的文件为包含进的文件, “-” 后面的内容为排除在外的文件。
- 合并元数据
完成插装操作以后,在指定的路径下会产生一些名为 “*coverage.em” 的文件,这些文件保存了插装的元信息,这些信息主要是记录插装过程中的插装点在被测代码中的位置。如果在插装过程中,指定这些文件到同一文件的话, EMMA 默认将元数据进行合并。如果测试人员未指定路径,或者希望得到独立的元文件,这些文件将分别产生在默认或指定的目录下。测试人员还可以通过使用 “merge” 命令手动将这些元文件进行合并,保证生成的覆盖率报表的全面性。注意:合并操作不支持逆向操作。
清单 5. 合并元数据命令
/opt/WebSphere/PortalServer/java/jre/bin/java emma merge -input <path1>/coverage1.em,<path2>/coverage.em -out <path>/coverage2.em
在 “input” 后面的参数为待合并的文件名,在 “out” 后面的参数为合并以后的结果文件。
完成上面的操作以后,就已经完成了收集覆盖率信息的准备工作。接下来测试人员可以进行正常的测试工作,在运行测试的过程中, EMMA 将跟踪并记录执行轨迹,得到覆盖率信息。
运行测试用例,得到覆盖率报告
完成插装工作以后,测试人员可以按照测试计划运行测试用例。 EMMA 将在测试执行的过程中记录代码执行信息并将结果记录在内存中。每次当 JVM 停止时,内存中记录的执行信息将被清除并被保存到 “*.ec” 的文件中。但是在实际测试的过程中, JVM 的停止很难控制,因此测试人员可以定时手动将内存中执行信息写出。在这种情况下,内存中的记录被输出,但是内存中的内容不被清除。清单5-7介绍了收集覆盖率信息以及生成覆盖率信息报告的命令。
清单 6. 从远程机器上收集覆盖率信息
/opt/WebSphere/PortalServer/java/jre/bin/java -cp emma.jar emma ctl –connect auscsdpfvtvm15.bto.ibm.com:47653 -command coverage.dump,/root/emma/Number_coverage.ec EMMA: processing control command sequence EMMA: executing [coverage.dump (/root/emma/Number_coverage.ec,,true)] EMMA: coverage.dump: runtime coverage data remotely merged into [/root/emma/Number_coverage.ec] {in 83 ms} EMMA: coverage.dump: command completed in 96 ms EMMA: control command sequence complete
清单 7. 从本地收集覆盖率信息
/opt/WebSphere/PortalServer/java/jre/bin/java -cp emma.jar emma ctl -connect auscsdpfvtvm15.bto.ibm.com:47653 -command coverage.get,/root/emma/Number_coverage.ec EMMA: processing control command sequence EMMA: executing [coverage.get (/root/emma/Number_coverage.ec,true,true)] EMMA: coverage.get: local copy of coverage data merged into [/root/emma/Number_coverage.ec] {in 39 ms} EMMA: coverage.get: command completed in 79 ms EMMA: control command sequence complete
这样收集到的信息被保存在 “coverage.ec” 中, “coverage.ec” 是二进制格式的文件,因此很难被用来查看覆盖率结果。
清单 8. 生成覆盖率报告
/opt/WebSphere/PortalServer/java/jre/bin/java -cp emma.jar emma report -r html -in /root/emma/Number_coverage.em,/root/emma/Number_coverage.ec - Dreport.html.out.file=/root/emma/Number_coverage.html -Dreport.metrics=class:80 (,method:75) EMMA: processing input files EMMA: 2 file(s) read and merged in 42 ms EMMA: writing [html] report to [/root/emma/coverage/Number_coverage.xml]
在生成覆盖率报告的过程中,测试人员可以根据测试要求通过 “Dreport.metrics” 参数设定满意的覆盖率标准。在示例命令中设定了类覆盖率的满意度为80%。
测试报告可以以 HTML ,文本和 XML 三种格式输出。图1、图2为 HTML 格式的报告的例子。覆盖率的报告是以包、类、方法三级单位组织的。图1是 Index 类的执行情况,其中红颜色代表该覆盖率未达到满意的覆盖率标准。图2则是包 org.numberquiz 中 QuizBran 类的执行情况,从总体看,类覆盖率为100%,方法为91%。在附录中可以看到示例程序完整的测试覆盖率报告。
图 1. Index 测试报告
图 2. QuizBran 测试报告
在功能测试过程中,为每个单独的测试用例生成独立的覆盖率报告能够给测试过程带来很大的帮助:
- 当测试用例失败或者抛出异常时,可以通过覆盖率报告找到该测试用例对应的代码,这样就可以为测试人员提供可能出错代码的范围。这一报告不仅可以帮助测试人员在提交问题时更加详细的描述错误,提供更详细的信息,还可以为开发人员跟踪问题提供线索,缩短解决问题的周期。
- 测试人员可以从独立的测试报告中获得代码和功能模块的对应关系,更好的理解测试用例的作用。
- 独立的测试报告可以帮助测试人员改进测试用例的设计,删除重复的测试用例,将覆盖点较多的测试用例进行拆分。
为得到独立的测试报告,需要在每次执行测试用例前,将内存中的执行信息清除。目前有两种方法支持清除记录,测试人员可在测试过程中,根据需要选择合适的方法。
- 每次运行完一个测试用例,重启 JVM 。这种方法能够完整的清除内存中记录的执行信息,但是每次重启 JVM 给测试带来很多麻烦。
- 使用 “coverage.reset” 命令,该命令可以在不重启 JVM 的情况下,清除内存中记录的方法、块、行的执行信息,但是无法清除类覆盖信息。如果用户关注的重点在方法的覆盖信息上,可以选择这种方法。
清单 9. 清除内存中覆盖率信息命令
/opt/WebSphere/PortalServer/java/jre/bin/java -cp emma.jar emma ctl -connect auscsdpfvtvm15.bto.ibm.com:47653 -command coverage.reset EMMA: processing control command sequence EMMA: executing [coverage.reset ()] EMMA: coverage.reset: coverage reset for 5 classes {in 0 ms} EMMA: coverage.reset: command completed in 31 ms EMMA: control command sequence complete
合并覆盖率结果
完成所用的测试用例后,测试覆盖信息可以被合并在一起,得到整个测试的覆盖报告。覆盖率结果文件通过 “merge” 命令合并 “*.ec” 文件实现的。
另外,由于 EMMA 中测试覆盖率是通过与 “*.em” 文件关联获得代码信息的,因此当代码发生变化时,已经运行过的测试不必完全重复,只需将得到的 “*.ec” 文件合并(新得到的 “*.ec” 文件放在后面),然后关联最新的 “*.em” 文件即可得到代码变化后的覆盖率信息,这方便了 EMMA 支持版本变化的测试。在生成新的测试报告的时候,需要注意 “*.ec” 的时间一定要晚于 “*.em” 文件。
清单 9. 合并覆盖率结果命令
/opt/WebSphere/PortalServer/java/jre/bin/java emma merge –input coverage1.ec,coverage2.ec,coverage3.ec –output coverage.ec
如果在生成测试报告的时候,如果出现 “com.vladium.emma.EMMARuntimeException: [CLASS_STAMP_MISMATCH] runtime version of class in the coverage data is not consistent with the version of this class in the metadata, possibly because stale metadata is being used for report generation” 错误,说明在生成新的 “*.em” 前后代码曾经被修改过,并且被修改的代码所在的类文件在新的测试中没有被覆盖到,这就需要重新执行这部分测试,保证修改过的部分被重新执行。
使用覆盖率报告总结和评估测试过程
到目前为止,针对每个测试用例的测试覆盖率报告和测试整体的覆盖率报告都已经得到了。这些报告可以帮助测试人员总结和分析测试结果,改进测试设计。
目前的功能测试中,测试人员主要借助执行的测试用例数目和测试过程中的问题报告来评价测试过程,因此,测试用例的设计直接关系到测试的充分性。测试人员往往无法从目前的测试结果报告中得到哪些部分被覆盖了,哪些部分未被覆盖的信息,也就造成了在测试结束时测试人员对测试结果没有信心。另外,很多情况下,测试人员完成了某些特定情况的测试,异常情况往往被忽视。
覆盖率报告为测试人员查看测试覆盖情况提供了清晰的视图。尽管100%的覆盖率不能证明没有问题,但是它为测试人员提供了参考。当覆盖率很低的时候,测试人员可以通过覆盖率报告的提示找到原因。
与开发人员讨论测试结果
在覆盖率结果、测试用例设计和执行文档的指导下,测试人员可以更清楚地与开发人员讨论测试结果,可以更加清楚的发现:
- 测试人员忽略的部分。
- 测试用例设计中被覆盖而测试执行中未覆盖的部分。
- 程序中的无用代码。
例如:在实际的测试过程中,发现一个叫 syslog.messages 包的覆盖率一直很低,一些类和方法始终没有被覆盖到。在测试执行过程中,与这些类相关的结果也没有出现。因此,在与开发人员确认后,发现对这些内容的调用在写代码的时候被遗忘了(图3中红色框中的内容)。
图 3. 覆盖率报告分析
改进测试设计
利用覆盖率报告,测试人员可以改进测试用例的设计:
- 移除覆盖范围重复的测试用例。
- 对于覆盖点过多的测试用例,可以进行拆分,保证测试用例具有针对性。
- 对于测试中未覆盖的部分,增加测试用例保证测试完整性。
结论
在功能测试中,测试人员一般不能直接获得被测对象的源代码,类、方法覆盖率可以作为衡量和评估测试的重要标准。 EMMA 在字节码插装的基础上捕获并报告测试中的覆盖率信息,使覆盖率成功地用于功能测试。通过在功能测试中使用 EMMA 可以改进测试设计,帮助问题分析,并能更好地评估测试,保证测试质量。