_有的时候宁愿付钱让你一周在床上待着,也不想让你用这周剩下的时间去调试你在周一所写的代码。 --丹·所罗门_
##2.14.1 测试驱动开发
做正确的事,比把事情做正确更为重要。
当明确需要做何事后,再通过事先编写单元测试来准确表达我们将要实现的功能,是相当具有指导意义的。你会发现接下来你的开发历程就是:单元测试-设计-重构,而且这种正向循环是很有创造性的,并且进行到一定程度后会慢慢体会到浮现式设计的乐趣。
关于测试驱动开发TDD,有很多资料已进行了说明,这里不再赘述。
如果还没了解PHPUnit,可先阅读:[PHPUnit 手册](https://phpunit.de/manual/3.7/zh_cn/automating-tests.html)
如果还没了解PHPUnit,可先阅读:[PHPUnit 手册](https://phpunit.de/manual/3.7/zh_cn/automating-tests.html)
如果还没了解PHPUnit,可先阅读:[PHPUnit 手册](https://phpunit.de/manual/3.7/zh_cn/automating-tests.html)
##2.14.2 意图导向编程
![apic](http://webtools.qiniudn.com/20150411005257_2bc8683a50ac1d620cef4e39918ed3e5)
在编写代码前,先写测试代码,更容易提高 **关注点** 。
因为,在开发过程中, 大多时候会被外界打断(如需求沟通、线上问题处理、临时会议等),而通过单元测试则可以让你“几乎忘却需要做什么”的情况下重新让你回到之前的状态,特别在并行开发多个不同项目的需求时尤其重要。
除此之外,遵循“红-绿-重构”这样的流程,我们可以在更高的层面关注需要实现的功能需求,并自顶而下地进行设计优化,精益代码。
##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接口层,是我们后台开发的主要切入点,也是直接对外提供服务的入口,属于更高层次的概念并拥有指定的业务功能,更是后台开发的关注点。所以在对新接口进行开发前,编写单元测试是非常有意义的。
为了可以自动生成测试代码,我们可以先简单定义好接口的函数签名(以获取用户基本信息接口为例):
```javascript
//$ vim ./Demo/Api/User.php
<?php
class Api_User extends PhalApi_Api {
public function getBaseInfo() {
}
}
```
随后,自动生成测试代码骨架:
```javascript
$ 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
```
根据接口的需要,验证接口返回的格式,以及业务数据的正确性。
```javascript
//$ vim ./Demo/Tests/Api/Api_User_Test.php
/**
* @group testGetBaseInfo
*/
public function testGetBaseInfo()
{
//Step 1. 构建请求URL
$str = '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
```javascript
//Step 1. 构建请求URL
$str = 'service=User.GetBaseInfo&user_id=1';
```
此参数即对应接口请求的URL参数,我们将此参数追加在接口入口并在浏览器打开可以得到同样的接口执行效果。但这样的好处更在于通过单元测试帮我们记住了各种接口测试的业务场景。而不再是像以前那样打开N个浏览器窗口人工进行调试,也不用像以前那样苦苦寻找浏览器记录。
如果接口需要POST数据,或者其他更多参数,可以使用$params来传递更多参数,一如:
```
//Step 1. 构建请求URL
$str = 'service=User.GetBaseInfo&user_id=1';
$params = array(); //更多参数
//Step 2. 执行请求
$rs = PhalApi_Helper_TestRunner::go($url, $params); //通过第二个参数,传送更多参数
```
####操作:执行请求
这里的操作,显然就是对应我们接口的调用。简单地如:
```javascript
//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、返回的业务数据是否正确;
```javascript
//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层的单元测试。
显然,这两层的开发,已经在前面的接口测试驱动开发的指导下很好地完成了。现在可以快速追加对这两层的单元测试。得益于我们的生成测试骨架的脚本,操作如下:
```javascript
$ 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**的引用路径:
```javascript
//$ vim ./Demo/Tests/Domain/Domain_User_Test.php
//$ vim ./Demo/Tests/Model/Model_User_Test.php
require_once dirname(__FILE__) . '/../test_env.php';
```
各自完善一下单元测试:
```javascript
//$ 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']);
}
```
执行一下:
```javascript
$ 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 更进一步的单元测试套件
到目前为止,我们有了如下的产品代码:
```javascript
dogstar@ubuntu:Demo$ tree
.
├── Api
│ └── User.php
├── Domain
│ └── User.php
├── Model
│ └── User.php
```
并拥有了与之平行对应的单元测试:
```javascript
dogstar@ubuntu:Tests$ tree
.
├── Api
│ └── Api_User_Test.php
├── Domain
│ └── Domain_User_Test.php
├── Model
│ └── Model_User_Test.php
└── test_env.php
```
这样是一个很好的开始,但若我们每次测试都分别调用三次这些不同层次的单元测试,显然有点不科学。所以,利用PHPUnit的配置文件,我们可以轻松管理我们的测试套件,如:
```javascript
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>
```
啊哈!终于,当需要调用这些分布在不同目录位置的单元测试时,只需要这么简单的一行命令:
```javascript
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.
- 欢迎使用PhalApi!
- 接口,从简单开始!
- [1.1]-下载与安装
- [1.2]-创建一个自己的项目
- [1.3]-在线体验
- [1.4]-文档、帮助和官网
- [1.10]-对PhalApi框架的抉择
- [1.11]-快速入门(backup)
- [1.12]-参数规则:接口参数规则配置
- [1.13]-统一的接口请求方式:_sevice=XXX.XXX
- [1.14]-统一的返回格式和结构:ret-data-msg
- [1.15]-数据库操作:基于NotORM的使用及优化
- [1.16]-配置读取:内外网环境配置的完美切换
- [1.17]-日记纪录:简化版的日记接口
- [1.18]-快速函数:人性化的关怀
- [1.19]-DI服务速查:各资源服务一览表
- [1.20]-DB操作:数据库基本操作速查
- [1.21]-类的自动加载:遵循PEAR包的命名规范
- [1.22]-签名验证:自定义签名规则
- [1.23]-请求和响应:GET和POST两者皆可得及超越JSON格式返回
- [1.24]-缓存策略:更灵活地可配置化的多级缓存
- [1.25]-国际化翻译:为走向国际化提前做好翻译准备
- [1.26]-数据安全:数据对称加密方案
- [1.27]-精益开发:更富表现力的Model层和重量级数据获取的应对方案
- [1.28]-COOKIE:对COOKIE原生态的支持及记忆加密升级版
- [1.29]-开放与封闭:多入口和统一初始化
- [1.30]-保持的力量:接口开发最佳实践
- [1.31]-新型计划任务:以接口形式实现的计划任务
- [2.11]-核心思想:DI依赖注入-让资源更可控
- [2.12]-海量数据:可配置的分库分表
- [2.13]-接口调试:在线SQL语句查看与性能优化
- [2.14]-测试驱动开发:意图导向编程下的接口开发
- [2.15]-演进:新型计划任务续篇
- [2.16]-领域驱动设计:应对复杂领域业务的Domain层
- [2.17]-微服务:Api接口服务层
- [2.18]-定制化:资源服务的再实现
- [2.19]-扩展库:可重用的扩展类库
- [2.20]-约定编程:架构明显的编程风格
- [2.21]-服务器统一部署方案简明版:CentOs---Nginx---php-fpm---MySql-[--Memcached]
- [2.22]-更多工具:精益项目和团队建设
- [3.1]-扩展类库:微信开发
- [3.2]-扩展类库:代理模式下phprpc协议的轻松支持
- [3.3]-扩展类库:基于PHPMailer的邮件发送
- [3.4]-扩展类库:优酷开放平台接口调用
- [3.5]-扩展类库:七牛云存储接口调用
- [3.6]-扩展类库:新型计划任务
- [3.8]-扩展类库:用户、会话和第三方登录集成
- [3.9]-扩展类库:swoole支持下的长链接和异步任务实现
- [3.11]-扩展类库:基于FastRoute的快速路由
- [4.2]-开发实战2:模拟优酷开放平台接口项目开发
- [4.3]-开发实战3:一个简单的小型项目开发(奔跑吧兄弟投票活动)
- [5.1]-架构与思想:PhalApi核心设计和思想解读
- [5.2]-杂谈:扯一些PhalApi的前世和今生
- [5.3]-框架总结:术语表和PHP开发建议
- [5.4]-许可
- [5.5]-联系和加入我们
- [5.6]-更新日记
- [5.8]-致框架贡献者:加入PhalApi开源指南
- [6.1]-基于接口查询语言的SDK包
- [6.2]-SDK包(JAVA版)
- [6.3]-SDK包(PHP版)
- [6.4]-SDK包(Objective-C版)
- [6.5]-SDK包(javascript版)
- [6.6]-SDK包(Ruby版)
- [8.1]-PhalApi视频教程
- 附录1:接口文档参考模板