Web端PHP代码函数覆盖率测试解决方案,php覆盖率
Web端PHP代码函数覆盖率测试解决方案,php覆盖率
1. 关于代码覆盖率
衡量代码覆盖率有很多种层次,比如行覆盖率,函数/方法覆盖率,类覆盖率,分支覆盖率等等。代码覆盖率也是衡量测试质量的一个重要标准,对于黑盒测试来说,如果你不确定自己的测试用例是否真正跑过了系统里面的每一行代码,在测试的完整性上总要打些折扣。因此,业界几乎对各种编程语言都有自己的一套代码覆盖率解决方案。世界上最美的语言PHP当然也不例外。PHPUnit和Spike PHPCoverage提供了一套基于xdebug的代码覆盖率测试方案。在本文中,我将针对自己碰到的特定业务场景,讲述一下自己进行PHP代码函数覆盖率测试的解决方案。
2. 业务背景
假设我们在线开发了一个网站,交给业务测试的同事去进行功能测试。那他们是怎么测试的呢?通常情况下,无非是开发人员把网站部署好了,然后测试人员把网上所有功能都试用一遍,包括一些异常使用情况。对于业务测试来说,只要我把所有的功能点都测了,把所有异常使用情况也测到了,那就完成了。但是对于开发来说,我比较好奇的是,你是否把我写的所有代码都跑到了?会不会存在一些代码,只有在很特殊的情况下才能触发,而你从来没有测到过这些情况?这时,可能就需要代码覆盖率来出马了。
其实我首先想到了xdebug来测试覆盖率,只需要两三个函数即可,如下:
xdebug_start_code_coverage(); //开始收集代码行覆盖情况 xdebug_get_code_coverage(); //获取截至目前所跑过的代码文件名和行号 xdebug_stop_code_coverage(); //停止收集代码行覆盖情况
xdebug提供的接口可以用于测试行覆盖率,这是否能满足要求呢?其实,行覆盖率颗粒度有点细,实际项目中,开发人员可能会对代码进行微调。比如,这次测试,你跑过了A.php文件的第10行,但是我有一天对A.php进行了微调,在A.php第9行和第10行之间又加了两行代码。于是,原来的第10行变为了第12行,而xdebug的行覆盖信息只记录了行号……这样之前的数据岂不是不准确了么。。。考虑再三,我觉得函数覆盖是个不错的颗粒度。在相对成熟的项目中,很少有大规模函数变动的情况。不过问题是,xdebug并没有提供函数覆盖的接口。
于是,我们现在碰到的场景是:
【1】希望测到某次测试中所覆盖的所有函数列表,知道这个项目总共有多少个函数,计算一下覆盖率是否足够高。
【2】测试完成之后,要生成一份覆盖率报告,将代码的覆盖情况可视化。
【3】完整测试的流程如下:
其中插桩的意思是在测试执行之前的一些准备工作。
3. 函数覆盖率解决方案
(1)原理
xdebug天生提供了对行覆盖率的支持,我们要自己计算出函数覆盖率。函数覆盖率需要两点数据,一个是哪些函数被执行,一个是文件中总共有多少个函数。
文件中总共的函数量,由于我们不可能把所有函数都执行一遍,因此这部分只能通过代码静态扫描来实现。如果是在C++或者Java中,可能就需要词法分析工具了,然而在最美的语言PHP面前,我们完全不需要那么复杂。从PHP4.3开始,PHP Zend Engine中内置了tokenizer功能,帮助开发者做源码词法分析。我们只需要找到PHP中定义函数时所对应的词法规律,就可以轻松得到指定PHP文件中的全部函数了。
tokenizer定义的接口也十分简单:
array token_get_all (string $source)
该函数进行文件解析,将php源代码拆成由token组成的数组。
string token_name (int $token)
将整数形式的token转变为字符串形式。类似于C语言中的strerror函数。有了tokenizer,自己再根据php函数定义的规律和格式设计一个有限状态机,即可完成全量函数的解析。这部分代码,本人写了个比较简陋的,把它单独拿出来,仅供大家参考:PHPFunctionParser
求函数覆盖率的另外一个难点在于获取被执行的函数列表。这地方让我们走了一些弯路。一开始一个最简单的办法,我们既然通过xdebug拿到被执的行,可以通过行号来反推此行属于哪一个函数。然而每一次的请求获取的行号信息量是非常大的,如果一个求情执行了1000行,那就要进行1000次判断,效率上会比较差。调研了一番之后,发现xdebug提供了function trace的功能,可以把一次请求中的函数调用关系获取到,只不过拿到了函数名字,却没办法得到它所在的文件。于是,再次调研一番,发现了Reflection,给定方法名和类名,可以反推出来它在哪个文件中定义。于是我们使用function trace把函数调用关系暂存在一个临时文件中,然后通过文件解析,拿到执行的函数名(如果是类方法,则是“类名::函数名”的形式),再通过reflection机制反推出定义这个函数的文件即可。再次体会到了世界上最美语言的强大之处。
(2)插桩
为了降低使用门槛,我们尽可能少地改变PHP源代码为好。xdebug收集信息的原理是分别调用xdebug_start_code_coverage和xdebug_stop_code_coverage来控制覆盖率信息收集的开始和结束,因此不可避免地要改变源代码。此处我们的解决办法是,将xdebug_stop_code_coverage通过register_shutdown_function注册为php程序结束前必须要跑的一段程序(类似C语言的atexit函数),将其封装到一个文件中,然后在源代码第一行require这个文件即可。如果你的PHP框架是CodeIgniter这种所有请求都有一个统一入口index.php的框架,那就只需要改变这一个文件即可,对源代码只有一行的改动!实际上,目前基本上所有的PHP框架,都是以一个index.php文件作为所有请求的入口。
我们对源代码的改动只有入口文件index.php的第一行加入了一句话:
require_once