作者: 孙天衣,于清国,石俊娟,沈燕玉
背景
代码覆盖率是衡量产品测试效果很重要的指标。得到单元测试的代码覆盖率相对比较简单。然而,web应用的测试人员经常会为收集集成测试或者端到端测试的代码覆盖率而伤脑筋。其中的主要原因是测试人员往往对这个领域的技术比较陌生,而且现有的方案比较复杂,容易出错。举例来讲,目前有一个方案不是很自动化,需要用户手工修改很多地方。我们经过调研,决定开发一个基于云技术的自动的一站式解决方案来收集端到端测试的代码覆盖率。我们开发的这个方案名为“ICoCo”(这个名称的意思是集成测试代码覆盖率(Integration Code Coverage))。
下面对一个代码覆盖率的工具JaCoCo做一个简单的介绍,因为ICoCo就是基于JaCoCo来开发的。JaCoCo一个非常突出的功能就是其能够利用一个代理在代码运行的过程动态注入指令来收集代码覆盖率。用户不要提前在项目的pom文件或者其他配置文件中做任何的修改。他们需要做的仅仅是在web容器(tomcat或其他)的启动参数中加上一行命令:“-javaagent:${DIR}/jacocoagent.jar=output=tcpserver,port=*,address=*”当然,目录、端口需要指定。 而且,代码覆盖率的结果很容易下载下来,因为Jacoco的代理启动了一个TCP的服务器。请参阅附录[3]获取更多的有关JaCoCo的信息。
利用持续集成工具来运行测试脚本是目前一种非常普遍的做法。我们目前使用的是Jenkins服务器。CI job被创建出来执行测试任务。我们的方案和Jenkins服务器作了无缝的集成。关于Jenkins的详细信息,请参阅附录[1]和[2]
概述
我们不仅利用了JaCoCo的功能,而且还利用了公司内部的云平台的API来解决应用的安装,部署问题。
如上所述,既然所有的测试工作都是在Jenkins服务器上执行,我们决定开发一个Jenkins 插件来完成所有的代码覆盖率的收集工作。Jenkins 插件很容易与现有的测试任务集成。现有的测试任务不用做任何修改,只要新建一个使用ICoCo的CI job, 把先用的测试任务的链接配置进去即可。关于如何开发Jenkins的插件,请参阅附录[4]。 对ICoCo的设计需求如下:
1) 能够自动把被测试的应用部署到测试的服务器。
2) 能够把JaCoCo的代理下载并修改web 容器启动参数以加载代理;
3) 能够合并子项目的代码覆盖率的结果。
4) 能够很容易扩展来支持不同种类的web 应用。
5) 以上所有的工作能够自动完成。
除了以上功能需求,我们在开发过程中尽可能复用已有的库或者服务,避免“重新发明轮子”。
目前这个方案不仅能够支持单个应用的测试,而且也能够支持多个应用的测试。
下图是ICoCo的配置界面图。我们可以看到配置非常简洁。
Figure1
下图是集成测试的代码覆盖率在Sonar上的展示图。从图中可以看到,Sonar能够把集成测试的代码覆盖率和单元测试的代码覆盖率很好的汇聚起来。目前公司要求所有的集成测试和端到端测试都要收集代码覆盖率,ICoCo帮助测试人员很好的完成了任务。
Figure2
ICoCo基本工作流程
1. 如有需要,部署web应用到服务器。用户需要在部署之前编译和上传安装包。这一步是可选步骤。如果用户针对服务器上当前版本测试,这一步可忽略。
2. 下载Jacoco 代理,修改启动参数,并重启web应用来启动Jacoco 代理。
3. 触发测试的CI job以进行端到端测试。在测试的过程中,JaCoCo 代理记录下代码覆盖率。
4. 利用JaCoCo的maven 插件下载代码覆盖率结果。
5. 如有需要,合并代码覆盖率结果。
代码覆盖率结果生成之后,如有需要,可以利用Sonar的Jenkins 插件把结构上传到Sonar的服务器以进行测试质量监控。
架构和实现细节
ICoCo的实现为Jenkins 插件。其架构是典型的三层架构。值得注意的地方是如果需要Jenkins plugin在一个分布式的Jenkins系统上工作,有一些特别的处理。细节请参考参考文档[4]。因为有一些公司内部的API,这个项目的源代码不会被公开。下面会给出一些代码示例作为参考。
1. 表现层 ICoCo的界面允许用户配置各种相关参数,如是否部署,测试CI job链接等。
Jenkins是通过一个称之为Jelly文件的配置文件来实现插件的界面开发。下面是一个实例代码。具体细节可参阅参考文档[5]。
<j:jelly xmlns:j="jelly:core"xmlns:st="jelly:stapler" xmlns:d="jelly:define"xmlns:l="/lib/layout"xmlns:t="/lib/hudson" xmlns:f="/lib/form">
<f:entry title="Name" field="name">
<f:textbox />
</f:entry>
</j:jelly>
2. 控制层 ICoCo的逻辑的主要入口点为ICOCOBuilder 类中的perform 方法。ICOCOBuilder类继承了Hudson.Task.Builder 类。各种功能,比如部署,安装JaCoCo 代理,触发测试,收集测试覆盖率等等,全部实现在perform方法中。界面输入的各个参数的值被传递到ICOCOBuilder中的相应的成员变量中, 以供各个功能使用。
3. 接口层 接口或者服务在各种功能执行的时候被调用。
a) 云中负责部署的API在部署的过程中被调用;
b) 云的管理接口被调用来下载和安装JaCoCo 代理,没有使用SSH直接链接。原因主要是避免输入用户名,密码。
c) Jenkins服务器的API被调用来触发测试的CI job;
d) JGit库的API被用来访问git服务器来下载源代码,用以生成代码覆盖率报告。代码示例如下:
public void downloadSourceFromGitRepo(File localRepoDir, String gitUrl, String commitId) throws IOException {
if (localRepoDir.exists() == true) {
FileUtils.delete(localRepoDir, FileUtils.RECURSIVE);
}
Git localRepo = null;
try {
localRepo = Git.cloneRepository()
.setURI(gitUrl)
.setDirectory(localRepoDir)
.call();
localRepo.checkout().setStartPoint(commitId).setName(commitId).call();
} catch (Exception e) {
RuntimeException re = new RuntimeException("Git clone failed in method downloadSourceFromGitRepo");
re.initCause(e);
throw re;
}
finally {
if (localRepo != null) {
localRepo.close();
}
}
OutputLogger.remoteLogging("Downloading source code done.");
e) Jacocomaven 插件被调用用来生成最终的代码覆盖率报告。
下面是Jacoco Maven 插件的调用代码示例:
-q -Dmaven.test.skip=true -Denforcer.skip=true org.jacoco:jacoco-maven-plugin:0.7.0.201403182114:dump -Djacoco.destFile=target/" + destFile + " -Djacoco.port=8084 -Djacoco.address=" + host + " org.jacoco:jacoco-maven-plugin:0.7.0.201403182114:report -f " + pomPath;
f) 合并子项目的代码覆盖率的下载结果。合并代码覆盖率调用了Jacoco的API。代码示例如下:
private static class MergeCCCallable implements Callable<Boolean, IOException> {
/**
*
*/
private static final long serialVersionUID = 1L;
private String rootFolder;
public MergeCCCallable(final String rootFolder){
this.rootFolder = rootFolder + File.separator;
}
public Boolean call() throws IOException {
mergeJacocoResult();
return Boolean.FALSE;
}
public void mergeJacocoResult() throws IOException {
List<File> fList = getCCFileList();
if (fList.size() == 0) {
throw new RuntimeException(
"No code coverage result file has been found!");
}
final ExecFileLoader loader = new ExecFileLoader();
for (final File f : fList) {
loader.load(f);
}
final File disFile = new File(rootFolder + "target/icoco.exec");
loader.save(disFile, false);
}
public List<File> getCCFileList() {
final List<File> res = new ArrayList<File>();
// collect result files in the target folder of root folder
String rootFld = rootFolder + "target";
OutputLogger.remoteLogging(rootFld);
File folder = new File(rootFld);
File[] list = folder.listFiles();
if (list != null){
for (final File f : list) {
if (f.getName().matches("^icocoTmp.*.exec")) {
res.add(f);
}
}
}
return res;
}
}
Figure4
小结
到目前为止,端到端测试中都应用了这个解决方案,取得了不错的效果。下载,安装配置非常简单,用户不要对代码覆盖率的收集细节有深入的了解,节约了大量的时间。
参考资料
1. The official web site forJenkins: http://jenkins-ci.org/
2. The description about Jenkinson Wikipedia: http://en.wikipedia.org/wiki/Jenkins_%28software%29
3. http://www.eclemma.org/jacoco/
4. http://ccoetech.ebay.com/tutorial-dev-jenkins-plugin-distributed-jenkins
5. https://wiki.jenkins-ci.org/display/JENKINS/Basic+guide+to+Jelly+usage+in+Jenkins