PHP编程之PHPUnit单元测试的应用

单元测试是一门注重实践的开发方式。虽然前面介绍了不少理论知识,但也是为了能让大家知其然,知其所以然。这一节将来介绍如何在平时开发中应用实践单元测试。

1 为Bugfixed编写单元测试用例

维护历史遗留系统的成本是很高的。这是因为旧系统经过几年的迭代,并且中间交接多个团队来维护,至今已经复杂到连曾经参与开发的人都难以理解。其次,虽然旧系统还可以继续运行,但时不时会出现一些问题,可能是功能需求未能充分满足,也可能是非功能性的需求达不到要求。最后,当然也是没有单元测试的。
如果你正在维护一个历史遗留系统,刚好它又出现问题了。先别急,也许这是一件好事。问题即机遇。采用与以往不同的解决方式,我们不仅能很好地解决当前这个问题,还能渐而远之,优化整个旧系统更大的问题。有时,要做一个敢搬石头的人。
旧系统是复杂的,它里面有很多业务规则,很多控制开关,很多嵌套层级,还充斥着很多临时粗糙的解决方案。为了能快速、有效解决所发现的缺陷,应坚持一个原则:改什么,测什么。

图1 改什么,测什么,图片素材摘自网络

犹如在一个管道系统里面,哪个部件坏了,需要换一个新的元件。只要能保证新的元件,进来的口径以及输出的口径和将要替换的旧元件相符合,就可以完美替换。将新的元件融入到原来的旧系统中。对于旧系统的缺陷bugfixed也一样,我们可以编写新的接口,并对其进行充分测试,保证接口签名、参数列表和原来的一样,确保输出的结果、格式也和原来的一样,那么我们就有信心能实现顺利的替换,并修复已有的问题。

对旧系统的Bugfixed编写单元测试,是在点的应用;而为重构优化编写单元测试套件,则是线的应用。
仍然是以旧系统为主,在历史遗留系统中,到处是比长城还长的代码。有很长很长的事务型脚本,也有很大很大的上帝类。它们都是不分层级、不分关注点、不分查询和命令操作的,代码晦涩难懂,模型混乱不清。根据领域驱动开发,结合重构的手法,可以对其进行先局部、再整体的重构和优化。为保证重构优化后的代码,不改变原有的功能和特性,可以使用单元测试来搭建360度的安全网。
这样,原来一个PHP文件,就会分解成多个PHP文件。原来一个类,就会划分成多个高内聚、低耦合的小类。针对这些多个文件、多个小类进行单元测试,就可以构成一个模块的测试套件。
当整个测试套件都正常通过时,我们重构的信心也会随之大增。
例如,在我曾经参与的项目中,有一个车型搜索列表的功能模块,原来的做法是典型的事务型脚本写法,全部代码都放置在一个文件list.php里面。经过重构后,新的代码文件结构是:

$ tree ./list/
./list/
├── list_controller_class.php
├── list_query_class.php
 └── list_query_item_class.php

而与之平行的测试代码文件结构是:

$ tree ./tests/list/
./tests/list/
├── list_controller_class_Test.php
 └── list_query_class_Test.php

里面的实现细节,暂时可以不用关注。这里举的例子,重点在于如何为重构后的模块编写单元测试套件。

3 为新域编写单元测试体系

进入一家新公司,或者加入一个新的团队,也有时候是从零到一,研发一个新系统,启动一个新项目。这时,我们可以配套地为新域搭建单元测试体系。这是面的应用。
为整个系统编写单元测试,搭建测试体系,需要考虑的因素比较多。在初始化方面,可以将全部需要用到的通用桩、替身提前进行初始化,并且设定测试基境。在划分测试套件时,可以根据不同的业务线、模块之间的关系,组装不同的、多个测试套件。在代码测试覆盖率方面,可以在配置好xdebug和PHPUnit.xml的whitelist白名单后,就可以得到HTML、文本或者XML等格式的覆盖率报告。
此时,借助一键测试,可以快速运行整个系统的单元测试体系。下面来讲一下何谓一键测试。
当我们拥有越来越多的单元测试时,为了可以快速执行测试套件,我们可以通过PHPUint的XML配置文件来组织需要执行的单元测试。在此基础上,为了快速执行整个项目的全部测试套件(如各个产品线的测试套件),我们需要一个脚本:run_tests.sh。并放置以下shell脚本代码:

#!/bin/bash
function showInfo()
{
    echo ""
    echo "======================================= [ $1 ] ========================================"
    echo ""
    echo ""
}
curPath=$(cd "$(dirname "$0")"; pwd)

# TODO: 以下是一些调用的测试套件或指定的脚本 ...
phpunit -c $curPath/phpunit_suite_1.xml

phpunit -c $curPath/phpunit_suite_2.xml

phpunit -c $curPath/phpunit_suite_3.xml

当一键测试的脚本完成后,我们就可以不断把重要的、需要被执行的单元测试纳入此脚本,然后只需要跑一下该脚本,就可以快速看到类似以下的输出:

$./run_tests.sh 
PHPUnit 3.7.29 by Sebastian Bergmann.
Configuration read from /path/to/tests/phpunit_suite_1.xml
..
Time: 4.52 seconds, Memory: 5.25Mb
OK (2 tests, 0 assertions)

……

我们应该限制此脚本的总运行时间在5分钟之内,以便我们可以经常快速执行并得到我们希望得到的反馈:项目代码是否存在问题!尤其是发布分支代码上线前和线上BUG紧急修复时。
基于一键测试,我们就可以实现自动化测试,例如按照以下架构部署,搭建自动化测试系统。

图2 简明部署的自动化测试系统

4 与持续集成的联动

更为专业的自动化测试,应该与目前主流的持续构建进行集成。网上已经有很多这方面的资料,这里不再重复赘述,只作简单提及。
最后,我们再简单认识一下渐进式单元测试,对于初学PHPUnit的同学,会有所帮助。
很多时候,我们一开始不知如何编写单元测试,原因在于我们需要面临的测试场景较为复杂。主要的测试难点有:数据多变,业务复杂。如果我们把整个流程梳理成各个环节,便可以得到以下的通用过程:数据源 -> 数据获取 -> 数据处理 -> 待测试的结果。这是基于数据流整理的关键路径。
由此看出,数据源越不稳定,待测试的结果也就越不稳定。从而导致了在同一个测试环境上不同时间点执行的测试结果不一样,或者同一时间不同的测试环境执行的测试结果也不一样。但是,不稳定的数据可以在测试上使用内存数据库、测试替身、数据提供器等,在开发上可以使用分层结构、依赖注入、设计模式等方式解决。而复杂的业务正是我们需要验证的,更需要高度关注的热区。
鉴于此,我们除了可以利用已知的F.I.R.S.T.原则、构造-操作-检验(BUILD-OPERATE-CHECK)模式来指导如何快速编写和执行测试,我们还可以通过一些策略。我们根据从简单到复杂、由浅到深、逐步深入的策略来编写单元测试,并称之为渐进式单元测试。主要可以划分为三个阶段:

    • 1、对待测试的代码进行调用和执行,以确保没有低级的语法错误;
    • 2、对返回的结果进行结构格式验证,以确保返回的结果是符合指定类型和规范的;
    • 3、对业务的数据作精确验证,以确保业务功能是正常工作的;

如下面的测试代码片段:

public function testGetBgImgForNewUser()
{
        $data = new Query_Background();
        $data->userTypeTag = 'new_user';
        $data->appCacheRead = false;
        $data->requestTime = strtotime('2015-01-16 10:00:00');
//1、对待测试的代码进行调用和执行,以确保没有低级的语法错误;
$rs = Controller_Background_::getBgImg($data);
//2、对返回的结果进行结构格式验证,以确保返回的结果是符合指定类型和规范的;
$this->assertTrue(is_array($rs));
$this->assertArrayHasKey('index_top_img', $rs);
$this->assertArrayHasKey('index_right_img', $rs);
$this->assertArrayHasKey('index_bottom_img', $rs);
$this->assertArrayHasKey('index_bg_color', $rs);
//3、对业务的数据作精确验证,以确保业务功能是正常工作的;
$this->assertEquals('http://img.example.com/u2_A8k.png', $rs['index_top_img']);
$this->assertEquals('#112233', $rs['index_bg_color']);
$this->assertEquals('http://mg.example.com/u2_A8q.png', $rs['index_top_img']);
$this->assertEquals('#2344dF', $rs['index_bg_color']);
}

发表评论