有的时候宁愿付钱让你一周在床上待着,也不想让你用这周剩下的时间去调试你在周一所写的代码。
2.14.1 测试驱动开发
做正确的事,比把事情做正确更为重要。
当明确需要做何事后,再通过事先编写单元测试来准确表达我们将要实现的功能,是相当具有指导意义的。你会发现接下来你的开发历程就是:单元测试-设计-重构,而且这种正向循环是很有创造性的,并且进行到一定程度后会慢慢体会到浮现式设计的乐趣。
关于测试驱动开发TDD,有很多资料已进行了说明,这里不再赘述。
如果还没了解PHPUnit,可先阅读:PHPUnit 手册如果还没了解PHPUnit,可先阅读:PHPUnit 手册如果还没了解PHPUnit,可先阅读:PHPUnit 手册
2.14.2 意图导向编程
在编写代码前,先写测试代码,更容易提高 关注点 。
因为,在开发过程中, 大多时候会被外界打断(如需求沟通、线上问题处理、临时会议等),而通过单元测试则可以让你“几乎忘却需要做什么”的情况下重新让你回到之前的状态,特别在并行开发多个不同项目的需求时尤其重要。
除此之外,遵循“红-绿-重构”这样的流程,我们可以在更高的层面关注需要实现的功能需求,并自顶而下地进行设计优化,精益代码。
2.14.3 编写测试的原则、模式和指导
首先应该意识到,测试代码和生产代码一样重要。其次,测试代码也应该和生产代码一样被同步维护更新,这样才能保持生气,更大地发挥作用。只有当不断地对测试的代码进行修修补被,我们才能保持自动化测试这张“安全网”常新。
2.14.4 F.I.R.S.T.原则
- 快速 Fast
- 独立 Independent
- 可重复 Repeatable
- 自足验证 Self-validating
- 及时 Timely
2.14.5 构造-操作-检验(BUILD-OPERATE-CHECK)模式
这个模式也可以理解成:“当… 做…应该…”。其中,构造包括测试环境的搭建、测试数据前期的准备;操作是指对被测试对象的调用, 以及被测试对象之间的通信和协助交互;最后检验则是对业务规则的断言、对功能需求的验证。
2.14.6 如何编写高效测试代码
- 1、与产品代码分开,与测试代码对齐
- 2、利用测试骨架(phpunit-skelgen或者自定义生成器)自动生成测试代码
- 3、使用测试替身、测试桩构建昂贵资源、制造异常情况
- 4、每个测试一个概念
2.14.7 PhalApi开发下的单元测试
我们推荐在各自的项目代码中平行编写单元测试,并逐渐完善、保持同步。以下是进行单元测试的参考。
(1)Api接口层的单元测试
Api接口层,是我们后台开发的主要切入点,也是直接对外提供服务的入口,属于更高层次的概念并拥有指定的业务功能,更是后台开发的关注点。所以在对新接口进行开发前,编写单元测试是非常有意义的。
为了可以自动生成测试代码,我们可以先简单定义好接口的函数签名(以获取用户基本信息接口为例):
//$ vim ./Demo/Api/User.php
<?php
class Api_User extends PhalApi_Api {
public function getBaseInfo() {
}
}
随后,自动生成测试代码骨架:
$ mkdir ./Demo/Tests/Api -p
$ cd ./Demo/Tests/Api
$ php ./PhalApi/build_phpunit_test_tpl.php ./Demo/Api/User.php Api_User ./Public/init.php
$ php ./PhalApi/build_phpunit_test_tpl.php ./Demo/Api/User.php Api_User ./Public/init.php > ./Demo/Tests/Api/Api_User_Test.php
根据接口的需要,验证接口返回的格式,以及业务数据的正确性。
//$ vim ./Demo/Tests/Api/Api_User_Test.php
/**
* @group testGetBaseInfo
*/
public function testGetBaseInfo()
{
//Step 1. 构建请求URL
$url = 'service=User.GetBaseInfo&user_id=1';
//Step 2. 执行请求
$rs = PhalApi_Helper_TestRunner::go($url);
//Step 3. 验证
$this->assertNotEmpty($rs);
$this->assertArrayHasKey('code', $rs);
$this->assertArrayHasKey('msg', $rs);
$this->assertArrayHasKey('info', $rs);
$this->assertEquals(0, $rs['code']);
$this->assertEquals('dogstar', $rs['info']['name']);
$this->assertEquals('oschina', $rs['info']['note']);
}
上面的验证意思简单明了,结合 构造-操作-检验(BUILD-OPERATE-CHECK)模式 加以说明一下。
构造:构建请求URL
//Step 1. 构建请求URL
$url = 'service=User.GetBaseInfo&user_id=1';
此参数即对应接口请求的URL参数,我们将此参数追加在接口入口并在浏览器打开可以得到同样的接口执行效果。但这样的好处更在于通过单元测试帮我们记住了各种接口测试的业务场景。而不再是像以前那样打开N个浏览器窗口人工进行调试,也不用像以前那样苦苦寻找浏览器记录。
如果接口需要POST数据,或者其他更多参数,可以使用$params来传递更多参数,一如:
//Step 1. 构建请求URL
$url = 'service=User.GetBaseInfo&user_id=1';
$params = array(); //更多参数
//Step 2. 执行请求
$rs = PhalApi_Helper_TestRunner::go($url, $params); //通过第二个参数,传送更多参数
操作:执行请求
这里的操作,显然就是对应我们接口的调用。简单地如:
//Step 2. 执行请求
$rs = PhalApi_Helper_TestRunner::go($url);
这样,便可以在服务端模拟进行一次接口的请求调度,注意这里是在服务端进行的接口请求,而不是客户端。此外,如果需要传递更多参数,可以参考前面的示例。这里简单补充一下PhalApi_Helper_TestRunner测试辅助类的接口签名说明:
<?php
class PhalApi_Helper_TestRunner {
/**
* @param string $url 请求的链接
* @param array $param 额外POST的数据
* @return array 接口的返回结果
*/
public static function go($url, $params = array()) {
... ...
检验:验证
在对接口返回的结果中,我们可以这样依次进行正确性的验证:
- 1、先验证接口返回的格式是否正确,有无字段遗漏;
2、返回的业务数据是否正确;
//Step 3. 验证
$this->assertNotEmpty($rs);
$this->assertArrayHasKey('code', $rs);
$this->assertArrayHasKey('msg', $rs);
$this->assertArrayHasKey('info', $rs);
$this->assertEquals(0, $rs['code']);
$this->assertEquals('dogstar', $rs['info']['name']);
$this->assertEquals('oschina', $rs['info']['note']);
由于测试环境的数据变动频繁,所以我们可以针对个别的接口进行更精确的验证,而对类似列表获取这样的大批量的数据,则校验其结构格式。除此之外,还有一种情况也是需要纳入检验,即除了上面的正常请求情况下的 异常请求 。
接下来的即是之前文档里面所说的单元测试执行和接口开发,此处略。
(2)Domain层和Model层的单元测试
下面继续简单补充一下之前没谈及到的Domain层和Model层的单元测试。
显然,这两层的开发,已经在前面的接口测试驱动开发的指导下很好地完成了。现在可以快速追加对这两层的单元测试。得益于我们的生成测试骨架的脚本,操作如下:
$ php ./PhalApi/build_phpunit_test_tpl.php ./Demo/Domain/User.php Domain_User > ./Demo/Tests/Domain/Domain_User_Test.php
$ php ./PhalApi/build_phpunit_test_tpl.php ./Demo/Model/User.php Model_User > ./Demo/Tests/Model/Model_User_Test.php
接着,修改一下测试环境 test_env.php的引用路径:
//$ vim ./Demo/Tests/Domain/Domain_User_Test.php
//$ vim ./Demo/Tests/Model/Model_User_Test.php
require_once dirname(__FILE__) . '/../test_env.php';
各自完善一下单元测试:
//$ vim ./Demo/Tests/Domain/Domain_User_Test.php
/**
* @group testGetBaseInfo
*/
public function testGetBaseInfo()
{
$userId = '1';
$rs = $this->domainUser->getBaseInfo($userId);
$this->assertArrayHasKey('id', $rs);
$this->assertArrayHasKey('name', $rs);
$this->assertArrayHasKey('note', $rs);
$this->assertEquals('dogstar', $rs['name']);
}
执行一下:
$ phpunit ./Demo/Tests/Domain/Domain_User_Test.php
PHPUnit 4.3.4 by Sebastian Bergmann.
.
Time: 49 ms, Memory: 6.25Mb
OK (1 test, 4 assertions)
(3)Model层的单元测试
Model层的单元测试类似,不再赘述。
2.14.7 更进一步的单元测试套件
到目前为止,我们有了如下的产品代码:
dogstar@ubuntu:Demo$ tree
.
├── Api
│ └── User.php
├── Domain
│ └── User.php
├── Model
│ └── User.php
并拥有了与之平行对应的单元测试:
dogstar@ubuntu:Tests$ tree
.
├── Api
│ └── Api_User_Test.php
├── Domain
│ └── Domain_User_Test.php
├── Model
│ └── Model_User_Test.php
└── test_env.php
这样是一个很好的开始,但若我们每次测试都分别调用三次这些不同层次的单元测试,显然有点不科学。所以,利用PHPUnit的配置文件,我们可以轻松管理我们的测试套件,如:
<!-- dogstar@ubuntu:Tests$ vim ./phpunit_user_getbaseinfo.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
...
<testsuites>
<testsuite name="Test Suite">
<file>./Api/Api_User_Test.php</file>
<file>./Domain/Domain_User_Test.php</file>
<file>./Model/Model_User_Test.php</file>
</testsuite>
</testsuites>
</phpunit>
啊哈!终于,当需要调用这些分布在不同目录位置的单元测试时,只需要这么简单的一行命令:
dogstar@ubuntu:Tests$ phpunit -c ./phpunit_user_getbaseinfo.xml
PHPUnit 4.3.4 by Sebastian Bergmann.
.....
Time: 54 ms, Memory: 7.25Mb
OK (5 tests, 28 assertions)
2.14.8 这样的好处?
上面的过程,细节较多,而且需要实际操作的部分也比较多。对于之前没有接触过单元测试这块的同学,可能会有点迷茫,对于不愿意接受单元测试的同学来说更加枯燥。
然而,然而。 当我们把越痛苦的事情越早完成后,我们后面就顺畅多了。正如在某一次培训中的某一位敏捷开发的专家所说的: 要逐步对小问题做优化,而不是要等到大问题到来时再做变革 。
那这样的好处在于哪里呢?
这里不就理论回答,而是以我个人的经历来简单说明。
首先,正如上面所说的,单元测试帮你很好地记住并整理了各种接口测试的场景,而不用再像以前那样打开N个浏览器窗口逐个人工校对。其次,在单元测试的论证下我们可以更有信心地跟测试说、跟产品说、跟发布说我们的代码没问题,因为我们通过严格的单元测试,而不是人为主观上的想当然应该不会有问题吧。最后,也是最重要的,在后期的接口升级、改动和维护中,单元测试再一次为我们提供了保护,犹如一张安全网,涵盖我们改动的每一处代码。与此同时,对于重构也亦然。
但单元测试所带给你的,不仅仅是上面所说的简单这几点。更多地完全不一样的开发历程,而其中滋味和令人兴奋的体现,只有当你亲自去尝试才会明白其中滋味。So, try it by yourself.