对查询构造器进行调试并不难,从其构造 SQL -> 数据绑定 -> SQL 执行的过程当中就能发现,要方便调试,只要能够观察如下信息:php
PDO 提供了一个方便的 debug 方法 PDOStatement::debugDumpParams() 来打印 SQL 和绑定的数据。咱们就使用它来作 debug 的工做。html
在基类添加 _debug 属性和 withDebug() 方法:git
protected $_debug = FALSE; ... public function withDebug() { $this->_debug = TRUE; // 方便链式调用,返回当前实例 return $this; }
修改 _execute() 方法:github
protected function _execute() { try { $this->_wrapPrepareSql(); $this->_pdoSt = $this->_pdo->prepare($this->_prepare_sql); $this->_bindParams(); $this->_pdoSt->execute(); $this->_reset(); // 若是是 debug 模式,则打印相关信息 if($this->_debug) { $this->_pdoSt->debugDumpParams(); // 打印 debug 信息 $this->_debug = FALSE; // debug 只在当此访问有效,打印完就关闭 } } catch (PDOException $e) { // when time out, reconnect if($this->_isTimeout($e)) { $this->_closeConnection(); $this->_connect(); // retry try { $this->_wrapPrepareSql(); $this->_pdoSt = $this->_pdo->prepare($this->_prepare_sql); $this->_bindParams(); $this->_pdoSt->execute(); $this->_reset(); // 若是是 debug 模式,则打印相关信息 if($this->_debug) { $this->_pdoSt->debugDumpParams(); // 打印 debug 信息 $this->_debug = FALSE; // debug 只在当此访问有效,打印完就关闭 } } catch (PDOException $e) { throw $e; } } else { throw $e; } } }
这样,在任何一个语句构造过程当中使用 withDebug() 方法 (在 get()、row() 等取结果的方法调用以前),就能打印出 debug 的信息。web
注:由于我在常驻内存模式下使用,因此选择直接打印到 stdout 中,这样能够直接在终端界面上调试。传统 web 模式中可使用 Output Control 系列函数 来获取 debug 信息。
从项目的角度看:sql
当项目的规模很小的时候,单元测试没什么用。可是若是是写底层框架或者项目发展到必定的规模时,单元测试对于提升生产力有很明显的贡献。shell
从程序设计的角度上看:数据库
单元测试可让你更好的拆分程序为最小单元,帮助你更好的解耦。服务器
单元测试的好处是给开发人员的,并非给机器的。composer
拿咱们编写的查询构造器为例,where()、get() 等方法依赖了不少底层的方法,底层方法之间也有互相的调用。
状况一:你要为一个底层方法添加功能,改完后如何判断是否会影响上层调用呢?把全部调用它的方法都调用一遍看结果吗?不用,只需使用单元测试,肯定这个方法的输入输出、可能的运行状况和边界状态,即保证最小单元可用。只要经过单元测试,则这个方法就没有问题 (固然这里的程序结构必须设计合理、测试必须准确有效)。状况二:有一天,你想为你的查询构造器再支持一个新的数据库,这个数据库的驱动类继承自基类。可是你不清楚基类的这些方法是否对新的数据库还有效 (好比 postgresql 中 lastInsertId 的不一样),要把全部方法跑一遍吗?不用,你只需事先把这些通用方法写好单元测试,把驱动类换做新数据库的驱动类执行单元测试便可,跑一遍你就会发现有哪些方法是有问题的。
状况三:和状况一相似,当一个方法出 bug 时,你并不能立刻定位此 bug 出在此方法仍是此方法依赖的方法上。并且当你定位了 bug 并进行修复时,发现其它方法由于修复出现了新的 bug,又是一轮 bug 查找。使用单元测试后,每次有修改后,都跑一遍单元测试,能够很快的发现这次修改对整个程序的影响,为咱们节省不少时间。
固然,单元测试中还有 stub、mock 之类的模式能够很好的解决依赖不肯定、难重现的问题,这里不作重点,咱们就很少说了。
到如今以前,咱们都是使用 test/test.php 这个文件写一些测试,这种简单的方式虽然作一些简单测试没有问题,可是完成单元测试就要大费周章了。而 PHP 有著名的单元测试框架 PHPUnit,能很好的完成咱们进行测试的需求,因此单元测试这块儿,咱们使用 PHPUnit。
安装 PHPUnit
PHPUnit 的安装很简单,在项目中执行:
composer require "phpunit/phpunit" "~4.0" composer require "phpunit/dbunit" "~2.0"
注:咱们的测试须要链接数据库,因此要安装 dbunit
如今在项目目录下的 test 文件夹中新建以 Test.php 结尾的测试文件,命令行运行 phpunit 便可运行测试。【1】
单元测试的代码简单、代码量大,我就不在这里展现了,全部的测试代码见 WorkerF - tests - DB。
固然,对于这个单元测试,仍是要作一些说明的。
单元测试的结构:
项目目录/ test/ PDODML.php PDODQL.php MysqlPDODMLTest.php MysqlPDODQLTest.php PgsqlPDODMLTest.php PgsqlPDODQLTest.php SqlitePDODMLTest.php SqlitePDODQLTest.php PDODriverTest.php test.xml testMysql.sql testPgsql.sql testSqlite.sql
PDODML.php 和 PDODQL.php 文件:
首先看 PDODML 和 PDODQL 类,包含了通用的 DQL 和 DML 方法的测试,经过原生 PDO 执行 SQL 得出的结果和查询构造器构造执行得出的结果相比较。
MysqlPDODMLTest.php、MysqlPDODQLTest.php 等数据库开头测试文件:
MysqlPDODMLTest 继承自 PDODML,MysqlPDODQLTest 继承自 PDODQL,Pgsql 和 Sqlite 一样道理。
MysqlPDODMLTest、MysqlPDODQLTest 这些测试类中使用 phpunit 的 setUpBeforeClass() 方法和 dbunit 的 getConnection() 方法等建立了一个全局可用的数据库链接,方便测试时对数据库的访问。
test.xml:
test.xml 中写好了 dbunit 要求的固定格式的模拟数据,用来测试时自动填充、恢复数据表 (由于 insert、update 等会更改数据表,这也是要用 dbunit 的缘由)。
PDODriverTest.php:
里面包含了对基类全部方法的测试。这里要说明一下,基类中有不少 protected 方法,个人测试方案是写一个新的类,继承自基类,而后新建 public 方法包裹要测试的 protected 方法,对新建的 public 方法进行测试,即达到了测试 protected 方法的目的。
这个文件中的测试更可能是测试各个方法构造的 SQL 字符串是否符合预期,使用的正则匹配断言比较多。
sql 文件:
几个数据库测试表的建表 sql。
本地测试
想要在你的本地跑这些测试的话,打开 MysqlPDODMLTest.php、MysqlPDODQLTest.php 等数据库开头的文件,把数据库配置中的 username、password、dbname 等改为你本身的便可。
Travis CI 提供的是持续集成服务(Continuous Integration,简称 CI)。它绑定 Github 上面的项目,只要有新的代码,就会自动抓取。而后,提供一个运行环境,执行测试,完成构建,还能部署到服务器。【2】
持续集成指的是只要代码有变动,就自动运行构建和测试,反馈运行结果。确保符合预期之后,再将新代码"集成"到主干。【2】
持续集成的好处在于,每次代码的小幅变动,就能看到运行结果,从而不断累积小的变动,而不是在开发周期结束时,一会儿合并一大块代码。【2】
若是你要将项目推到 Github 上,能够接入 Travis CI。经过编写 .travis.yml 配置文件,能够实现远程运行环境的语言多版本切换、软件安装、脚本执行等操做。
对于查询构造器这个项目,咱们可让其在远程运行环境安装相关数据库软件,执行数据表创建,数据导入,执行单元测试等操做。
个人框架项目 WorkerA 就集成了 Travis CI ,相关配置见 WorkerF - .travis.yml,感兴趣的能够了解下。
PHP 中对方法的注释一是为了提示,二是为了生成文档。我这里的注释写法是标明功能、参数、返回值和抛出的异常。一个清晰好懂的注释对于项目来讲仍是很必要的。
例如:
/** * get paginate data * * @param int $step * @param int $page * @return array * @throws \PDOException */ public function paginate($step, $page = NULL) { ... }
一个查询构造器的建立到此结束,但愿对你们有用。若是发现文中的书写和思路有错误,或者对此项目有什么好的建议的话,欢迎提出。对文中的解释有不解的地方,也欢迎提问。
查询构造器的完整代码:WorkerF - DB
查询构造器的单元测试完整代码:WorkerF - tests - DB。
【1】PHPUnit Doc