测试覆盖率 代码变更覆盖率平台 - 针对手工测试的代码变更覆盖率实现之路

Ramsey · 2019年05月09日 · 最后由 King 回复于 2024年05月23日 · 16701 次阅读
本帖已被设为精华帖!

作为一家小公司,日常测试基本都是以手工测试为主。如何在频繁快速的迭代过程中有效保证手工测试覆盖的充分性,是一个不得不思考的问题。

覆盖率是度量测试完整性的一个手段,也是测试有效性的一个依据。当一个迭代版本完成手工测试后,如果可以通过分析本次迭代变更代码的覆盖率来评估前期的测试用例设计是否完善,开发提交的改动 QA 这边是否均了解全面,开发代码是否存在冗余。从而更好的进行补充测试,提升本次上线的信心。

PS:当然首先要说明的是变更代码覆盖率并不能绝对作为测试全面的一个保证,因为即便测试覆盖了代码,也可能受测试人员素质和能力的影响出现漏测。但是我们可以认为高覆盖率的代码不一定质量高,但是低覆盖率的代码质量一定不高。

第一阶段:引入 jacoco 全量覆盖率

通过 jenkins 的 jacoco 插件来创建代码覆盖率任务,这个网上流程有很多,这里就不多做介绍了。当然自己在部署中也遇到了一些坑,下面是我之前遇到的坑的总结,https://testerhome.com/topics/16925

第二阶段:平台化实现代码变更覆盖率

在实现全量代码覆盖率后,发现作为一个庞大且迭代频繁的项目,全量的代码覆盖率意义并不大,因为我们要评估的每个迭代的测试情况,我们需要更精准的了解本次迭代的代码覆盖率情况,为了更好的让每个测试人员可以方便、高效的了解对应服务的覆盖率情况。从而开发了这个代码变更覆盖率平台。

变更代码覆盖率整体服务架构图

设计思路


  • 基于 python 的 git 第三方库,进行代码比对获取变更代码的情况
  • 基于 jacoco 的覆盖率报告,只对变更代码行进行颜色显示
  • 覆盖率通过任务的方式在平台上呈现,保证相同项目相同服务覆盖率任务后续操作的的便捷性
  • 将改动覆盖率报告写入平台 template 中,实现平台上直接可以查看到覆盖率文件

关键特征


  • 项目管理:后台对项目增删改操作,前台列表展示
  • 服务管理:后台对服务增删改操作,前台支持筛选展示对应服务的任务
  • 任务管理:支持对覆盖率任务增删改查操作
  • 版本比对:支持通过给出版本号或者通过 “HEAD~1” 方式描述回退版本数来指定与当前版本比对的版本
  • 覆盖率数据:手动触发获取对应服务的覆盖率数据,统计服务下所有包及类的变更代码覆盖率统计数据
  • 覆盖率详情:查看指定类文件的覆盖率详情,基于 jacoco 的覆盖率报告,显示规则基本一致,唯一区别对不属于变更的代码不会有底色显示
  • 权限管理:后台对账号控制项目权限,来控制对应人员只能看到制定的项目情况

本地环境部署


1.安装 mysql 数据库服务端 (推荐 5.7+),并设置为 utf-8 编码,创建相应 difftest 数据库,设置好相应用户名、密码,启动 mysql

2.修改:DiffTestPlatform/DiffTestPlatform/setting.py 里 DATABASE 的配置

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'HOST': '127.0.0.1',  #数据库所在服务器的ip地址
        'PORT': '3306',   #监听端口 默认3306即可
        'NAME': 'difftest',  #新建数据库名
        'USER': 'root',   #数据库登录名
        'PASSWORD': '123456',   #数据库登录密码
        'OPTIONS': {
            'init_command': "SET sql_mode='STRICT_TRANS_TABLES'",
        },
    }
}

3.命令行窗口执行 pip install -r requirements.txt 安装工程所依赖的库文件

4.命令行窗口切换到根目录 生成数据库迁移脚本,并生成表结构

python manage.py makemigrations CodeDiff #生成数据迁移脚本
python manage.py migrate  #应用到db生成数据表

5.创建超级用户,用户后台管理数据库,并按提示输入相应用户名,密码,邮箱。

python manage.py createsuperuser

6.启动服务

python manage.py runserver 0.0.0.0:8000  #本地环境启动可以配置0.0.0.0,如果是正式环境需要配置访问ip

具体平台说明请见 github

github 地址:https://github.com/hzlifeng1/DiffTestPlatform

共收到 42 条回复 时间 点赞

有没有针对 PHP 代码实现的例子呢?全是 java 的,参考不到啊

思寒_seveniruby 将本帖设为了精华贴 05月09日 15:37
我去催饭 回复

我们做了针对 JAVA、PHP 和 C++ 的全量增量覆盖率统计,php 用的 php-coverage 来实现的

simple 回复

C 也能实现手工测试覆盖率统计吗?我们的运行在硬件设备上,统计不起来。设备本身内存就小。

超级赞,给个好评再细看

不二家 回复

没试过 C 哦

Jenkins 和服务的部署需要在同一个机器上吗?如果不是,配置能否详细介绍一下,谢谢

Ramsey #41 · 2019年05月13日 Author
Su 回复

目前我上传的平台代码是只针对在同一台机器上的。当然因为我们的测试环境也部署了多个 jenkins,所以也会存在你说的服务需要去调用其他机器上的 jenkins 的情况,不过这部分代码我没放到 github 上。这块你可以考虑用把平台中获取覆盖率的数据和报告的代码提取出来部署在 jenkins 的服务器上,然后在平台上通过 python 的 paramiko 库去实现远程执行 linux 命令去触发覆盖率数据的获取和报告生成,再通过这个库的文件下载功能把覆盖率文件拉下来。不过这样子执行效率会比正常的慢一点。

非常好,点个赞,点亮小星星⭐

马克,借鉴下

38楼 已删除

这个是只针对 android 端的吗?

Su 回复

社区线下沙龙,来着 58 的讲师分享过分布式覆盖率统计的设计方案

Ramsey #35 · 2019年05月24日 Author
橘子 回复

不是,针对服务端的

请问下,任务名称、发布名称,具体怎么配置啊 ?

Ramsey #33 · 2019年05月28日 Author
hill 回复

项目在 github 的 readme 上有说明:任务名称为 jenkins 上创建的覆盖率任务的名称。发布名称为 jenkins 上服务发布的任务名称,比对版本为 git 上对应的版本的 commit 版本号。你新增任务的时候按照这个规范输入就好

想请问一下这个需求可以 cover 不,我在版本 v1 发布了一个应用,然后手工测试了这个版本的应用,jacoco 也有了报告 r1。然后开发修改了一些 bug,发布了 v2 的应用。经过测试后 jacoco 有了报告 r2.请问 r2 可以记录下我 r1 的报告做一个 merge 吗?

Ramsey #31 · 2019年05月29日 Author
Red_herring 回复

首先 jacoco 的统计的覆盖率情况是根据拉取 exec 文件来的,如果你要 v2 的报告也要统计 v1 的覆盖率数据,你可以在 jacoco 的 build.xml 文件里对每次拉取的 exec 进行 merge。这样统计出来的是历史以来的全量的覆盖率情况。如下:

<target name="merge_exec">
    <jacoco:merge destfile="/jenkins/workspace/xxxxxx/merged.exec">
        <fileset dir="/jenkins/workspace/xxxxxx" includes="*.exec" />
    </jacoco:merge>
</target>

当然在你在获取所有版本全量的覆盖率的情况下,再针对其中某个版本到最新版本的覆盖率,可以通过我的平台来生成。但是如果你直接要做整合多次覆盖率报告,只要修改 build.xml 就可以了。

Ramsey 回复

但是如果我在 v1 的时候生成的 exec,测试了 A 文件的 1-100 行,如果修改 bug 改了这 100 行,接着我测试了 101-200 行的 case,生成了 v2 的 exec。现在 v2 的 exec 和 v1 的 exec 对于 0-100 行的映射就不一样了吧。这种情况怎么用 merge 最后生成一个总的覆盖率报告呢?

Red_herring 回复

针对这个问题我之前也查过资料,我是这么理解的。生成 jacoco 报告的时候是会比对覆盖率文件和编译文件,假如两者代码不一致就不会显示覆盖。这就是为什么,明明你测试了,但是你覆盖率代码和编译代码不一致的情况下生成的覆盖率报告数据为空的情况了。所以即使你合并了覆盖率文件,当发现 100 行在覆盖率文件中虽然覆盖了,但是代码与源代码不一致,仍然不会显示为覆盖。

大神,想问下你们是怎么跟 jenkins、git 关联起来的,看你 github 上只是说 “任务名称为 jenkins 上创建的覆盖率任务的名称。发布名称为 jenkins 上服务发布的任务名称,比对版本为 git 上对应的版本的 commit 版本号 “ 这怎么关联

阿森 回复

我们全量覆盖率是 通过 jenkins 的 jacoco 插件来生成的,就是你需要在 jenkins 上面创建一个覆盖率任务用来生成全量覆盖率报告。而我们代码变更覆盖率报告就是基于这里生成的全量覆盖率报告而来的。
其中我们这里配置的覆盖率任务名称是为了得到对应覆盖率报告的路径。服务发布的任务名称也是为了在服务发布路径目录下获取 git 信息,从而得到当前 commit 的版本和比对版本的代码差异。

Ramsey 回复

嗯。有什么办法可以解决这种情况吗?想最后拿一个 merge 完了的报告去找开发一块讨论这块没有覆盖到的代码才有意义。不然一个一个看的话,开销太大了

Ramsey #25 · 2019年05月30日 Author
Red_herring 回复

理论上这个 merge 结果不就是你想要的吗,100 行第一次覆盖了,第二次因为代码改动,所以统计为没覆盖。

感谢分享,最近也想实践下~~

jenkins 上创建的覆盖率任务 -- 这个怎么弄的?

可以作为检测开发自测的一种手段了😷

Ramsey #21 · 2019年06月18日 Author
stevenxu 回复

这个具体网上都有教程,jenkins 上下载 jacoco 插件,然后创建一个任务,在任务中实现拉取源代码、编译代码和覆盖率文件,通过 jacoco 插件分析这些数据来获取覆盖率详情。

Red_herring 回复

你解决这个问题了吗

这个要支持一下,有支持 c# 代码的计划吗?

Su 回复

利用 linux 的同步功能把文件同步到一个机子上



上面两个图的名称不一样,无法反向生成 url 吧。

部署了一波,使用了下:针对覆盖了报告,大神为啥没在 index 页面放个入口呢?

simple 回复

大神,php 的能给分享下吗?

支持 python 下 针对 diff 文件进行覆盖统计吗

楼主,感谢分享~
想问下通过 jacoco 可以知道变更代码的影响范围吗?

Ramsey #12 · 2019年11月20日 Author
Janet 回复

老实说,这个做不到。因为你光看改动代码,你并不知道它会影响到多少函数。它只是作为补充测试一个手段

Ramsey 回复

是说覆盖率这些只是作为补充测试的一个手段吗

Ramsey #10 · 2019年11月25日 Author
Janet 回复

是的,覆盖了代码未必一定没问题,但是没覆盖的代码肯定可能存在遗漏

simple [精彩盘点] TesterHome 社区 2019 年 度精华帖 中提及了此贴 12月24日 23:00
simple [精彩盘点] TesterHome 社区 2019 年 度精华帖 中提及了此贴 12月24日 23:00
Ramsey jacoco 代码覆盖率使用中遇到的一些坑 中提及了此贴 02月26日 09:48
simple 回复

有 python 项目的麽

大佬,咨询个问题,公司没有使用 jenkins 部署,使用的是云效部署项目的,请问这个怎么搞?


这个 jenkins 是测试环境地址,不是本机的吧?

大佬,你的运行环境的工具版本都是多少呀,现在执行一步就错一步

ERROR: ResolutionImpossible: for help visit https://pip.pypa.io/en/latest/user_guide/#fixing-conflicting-dependencies
(venv) macdeMacBook-Pro-6:DiffTestPlatform mac$ pip3 install mysqlclient==1.4.2.post1
Collecting mysqlclient==1.4.2.post1
Using cached mysqlclient-1.4.2.post1.tar.gz (85 kB)
Preparing metadata (setup.py) ... done
Using legacy 'setup.py install' for mysqlclient, since package 'wheel' is not installed.
Installing collected packages: mysqlclient
Running setup.py install for mysqlclient ... error
ERROR: Command errored out with exit status 1:
command: /Users/mac/Desktop/Autotestplat/venv/bin/python -u -c 'import io, os, sys, setuptools, tokenize; sys.argv[0] = '"'"'/private/var/folders/rv/405qccgj5f53d6kl66zhzxnw0000gn/T/pip-install-v2jjx4kq/mysqlclient_26381edf041844fe81f195c2171ff75f/setup.py'"'"'; file='"'"'/private/var/folders/rv/405qccgj5f53d6kl66zhzxnw0000gn/T/pip-install-v2jjx4kq/mysqlclient_26381edf041844fe81f195c2171ff75f/setup.py'"'"';f = getattr(tokenize, '"'"'open'"'"', open)(file) if os.path.exists(file) else io.StringIO('"'"'from setuptools import setup; setup()'"'"');code = f.read().replace('"'"'\r\n'"'"', '"'"'\n'"'"');f.close();exec(compile(code, file, '"'"'exec'"'"'))' install --record /private/var/folders/rv/405qccgj5f53d6kl66zhzxnw0000gn/T/pip-record-q8mrkt6p/install-record.txt --single-version-externally-managed --compile --install-headers /Users/mac/Desktop/Autotestplat/venv/include/site/python3.6/mysqlclient
cwd: /private/var/folders/rv/405qccgj5f53d6kl66zhzxnw0000gn/T/pip-install-v2jjx4kq/mysqlclient_26381edf041844fe81f195c2171ff75f/
Complete output (35 lines):
/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/distutils/dist.py:261: UserWarning: Unknown distribution option: 'long_description_content_type'
warnings.warn(msg)
running install
running build
running build_py
creating build
creating build/lib.macosx-10.6-intel-3.6
creating build/lib.macosx-10.6-intel-3.6/MySQLdb
copying MySQLdb/init.py -> build/lib.macosx-10.6-intel-3.6/MySQLdb
copying MySQLdb/exceptions.py -> build/lib.macosx-10.6-intel-3.6/MySQLdb
copying MySQLdb/compat.py -> build/lib.macosx-10.6-intel-3.6/MySQLdb
copying MySQLdb/connections.py -> build/lib.macosx-10.6-intel-3.6/MySQLdb
copying MySQLdb/converters.py -> build/lib.macosx-10.6-intel-3.6/MySQLdb
copying MySQLdb/cursors.py -> build/lib.macosx-10.6-intel-3.6/MySQLdb
copying MySQLdb/release.py -> build/lib.macosx-10.6-intel-3.6/MySQLdb
copying MySQLdb/times.py -> build/lib.macosx-10.6-intel-3.6/MySQLdb
creating build/lib.macosx-10.6-intel-3.6/MySQLdb/constants
copying MySQLdb/constants/
init.py -> build/lib.macosx-10.6-intel-3.6/MySQLdb/constants
copying MySQLdb/constants/CLIENT.py -> build/lib.macosx-10.6-intel-3.6/MySQLdb/constants
copying MySQLdb/constants/CR.py -> build/lib.macosx-10.6-intel-3.6/MySQLdb/constants
copying MySQLdb/constants/ER.py -> build/lib.macosx-10.6-intel-3.6/MySQLdb/constants
copying MySQLdb/constants/FIELD_TYPE.py -> build/lib.macosx-10.6-intel-3.6/MySQLdb/constants
copying MySQLdb/constants/FLAG.py -> build/lib.macosx-10.6-intel-3.6/MySQLdb/constants
running build_ext
building 'MySQLdb._mysql' extension
creating build/temp.macosx-10.6-intel-3.6
creating build/temp.macosx-10.6-intel-3.6/MySQLdb
gcc -fno-strict-aliasing -Wsign-compare -fno-common -dynamic -DNDEBUG -g -fwrapv -O3 -Wall -arch i386 -arch x86_64 -g -Dversion_info=(1,4,2,'post',1) -D
version=1.4.2.post1 -I/usr/local/mysql-5.7.31-macos10.14-x86_64/include -I/Users/mac/Desktop/Autotestplat/venv/include -I/Library/Frameworks/Python.framework/Versions/3.6/include/python3.6m -c MySQLdb/_mysql.c -o build/temp.macosx-10.6-intel-3.6/MySQLdb/_mysql.o
gcc -bundle -undefined dynamic_lookup -arch i386 -arch x86_64 -g build/temp.macosx-10.6-intel-3.6/MySQLdb/_mysql.o -L/usr/local/mysql-5.7.31-macos10.14-x86_64/lib -lmysqlclient -o build/lib.macosx-10.6-intel-3.6/MySQLdb/_mysql.cpython-36m-darwin.so
ld: warning: The i386 architecture is deprecated for macOS (remove from the Xcode build setting: ARCHS)
ld: warning: ignoring file /usr/local/mysql-5.7.31-macos10.14-x86_64/lib/libmysqlclient.dylib, building for macOS-i386 but attempting to link with file built for macOS-x86_64
ld: warning: ignoring file /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/lib/libSystem.tbd, missing required architecture i386 in file /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/lib/libSystem.tbd (3 slices)
ld: dynamic main executables must link with libSystem.dylib for architecture i386
clang: error: linker command failed with exit code 1 (use -v to see invocation)
error: command 'gcc' failed with exit status 1
----------------------------------------
ERROR: Command errored out with exit status 1: /Users/mac/Desktop/Autotestplat/venv/bin/python -u -c 'import io, os, sys, setuptools, tokenize; sys.argv[0] = '"'"'/private/var/folders/rv/405qccgj5f53d6kl66zhzxnw0000gn/T/pip-install-v2jjx4kq/mysqlclient_26381edf041844fe81f195c2171ff75f/setup.py'"'"'; __file
='"'"'/private/var/folders/rv/405qccgj5f53d6kl66zhzxnw0000gn/T/pip-install-v2jjx4kq/mysqlclient_26381edf041844fe81f195c2171ff75f/setup.py'"'"';f = getattr(tokenize, '"'"'open'"'"', open)(file) if os.path.exists(file) else io.StringIO('"'"'from setuptools import setup; setup()'"'"');code = f.read().replace('"'"'\r\n'"'"', '"'"'\n'"'"');f.close();exec(compile(code, __file_, '"'"'exec'"'"'))' install --record /private/var/folders/rv/405qccgj5f53d6kl66zhzxnw0000gn/T/pip-record-q8mrkt6p/install-record.txt --single-version-externally-managed --compile --install-headers /Users/mac/Desktop/Autotestplat/venv/include/site/python3.6/mysqlclient Check the logs for full command output.
(venv) macdeMacBook-Pro-6:DiffTestPlatform mac$

File "/Users/mac/Desktop/DiffTestPlats/DiffTestPlatform/urls.py", line 18, in
from CodeDiff import views
File "/Users/mac/Desktop/DiffTestPlats/CodeDiff/views.py", line 5, in
from CodeDiff.util.run_diff import RunDiff
File "/Users/mac/Desktop/DiffTestPlats/CodeDiff/util/run_diff.py", line 6, in
from git import Repo
ModuleNotFoundError: No module named 'git'
(venv) macdeMacBook-Pro-6:DiffTestPlats mac$
各种报错,大佬方便回复下吧

大佬你这个项目不维护了吗

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册