PHP单元测试-mock和数据库测试

在计算机编程中,单元测试(英语:Unit Testing)又称为模块测试, 是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。

本文主要是根据PHPUnit(The PHP Testing Framework)文档结合实例简单介绍一下 PHP 单元测试中 mock 和数据库测试。如果你是初次接触单测的话建议先看一下 PHPUnit 文档中的入门章节

通常来说是开发程序和单测是同步进行的,项目提测的时候核心模块都需要包括单测(报告),但这个要求在不同的公司、部门、项目组要求不一样。虽然单测会占用一定的开发时间但总的来说单测是利远大于弊,最大的好处是自己或者别人后续更新模块功能时不用担心对原有功能造成了影响而不知情。

下面是一个具体的例子,在 web 开发中这样一个场景可能很常见:PHP 提供一个帐号注册的接口供前端调用,接口先检验一下此用户名是否已经存在,不存在的话插入数据库,返回注册成功。接口代码是这样的:

主要调用了 Join 类的 signIn 方法。我们来看看 Join 类是啥样:

逻辑很简单,先调用 Db 类的 exists 方法判断用户名是否存在,不存在的话使用 insert 方法插入数据。Join 类是这次业务新加的,比较重要,需要单测来保障质量,但这里用到了个 Db 类,这个库是以前就有的(坑),可能会影响本模块单测的正确性,而且 Db 类需要连接数据库,比较麻烦,这种场景就需要 mock 了。本文说的 mock 是广义上的,包括 Stubs(桩件)和仿件对象(Mock Object)。

将对象替换为(可选地)返回配置好的返回值的测试替身的实践方法称为上桩(stubbing)。可以用桩件(stub)来“替换掉被测系统所依赖的实际组件,这样测试就有了对被测系统的间接输入的控制点。这使得测试能强制安排被测系统的执行路径,否则被测系统可能无法执行”。

将对象替换为能验证预期行为(例如断言某个方法必会被调用)的测试替身的实践方法称为模仿(mocking)

我们这里应用的是打桩的概念。signIn 方法有两个分支:用户名存在和不存在。所以我们需要让 Db 类的 exists 方法在输入某个(些)用户名的时候返回 true。主要使用 PHPUnit_Framework_TestCase 类提供的 getMockBuilder() 方法来建立一个桩件对象:

代码看上去很像是实例化了一个类,其实原理也和这个差不多,PHPUnit 通过反射机制获取到类及其方法的信息,然后使用内置模板生成一个新类。我们需要 mock 掉 insertexists 方法:

这里使用了桩件生成器的 setMethods() 方法来设置哪些方法被上桩,以下是生成器提供的方法列表:

  • setMethods(array $methods) 可以在仿件生成器对象上调用,来指定哪些方法将被替换为可配置的测试替身。其他方法的行为不会有所改变。如果调用 setMethods(null),那么没有方法会被替换。
  • setConstructorArgs(array $args) 可用于向原版类的构造函数(默认情况下不会被替换为伪实现)提供参数数组。
  • setMockClassName($name) 可用于指定生成的测试替身类的类名。
  • disableOriginalConstructor() 参数可用于禁用对原版类的构造方法的调用。
  • disableOriginalClone() 可用于禁用对原版类的克隆方法的调用。
  • disableAutoload()可用于在测试替身类的生成期间禁用 __autoload()

然后分别设置两个方法的参数和返回值。这里 insert 操作比较简单,可以用 willReturn($value) 返回简单值:

上面的例子中,使用了 willReturn($value) 返回简单值。这个简短的语法相当于 will($this->returnValue($value))。而在这个长点的语法中,可以使用变量,从而实现更复杂的上桩行为。我们这里的需求是需要根据预定义的参数清单来返回不同的值,显然这是一个映射(map),PHPUnit 提供现成的 returnValueMap() 方法来做这个事情:

完整的单测代码: Continue Reading…

【译】简单介绍通过Predis库在PHP使用Redis

本文翻译自 《An Introduction to Redis in PHP using Predis》,已得到作者 @Daniel Gafitescu 的允许。原文版权归作者和 sitepoint.com 所有,如果你有原文使用需求请自行联系作者。

Redis 是一个开源的内存数据库服务器,得益于内建的数据类型,Redis 不仅仅只能做简单的 key/value 存储。

Redis 由 @Salvatore Sanfilippo 在 2009 年发布,因为其受欢迎和增长迅速,被很多大公司比如 VMware(后来聘请作者去全职工作)、GitHub、Craigslist、Disqus、Digg、Blizzard、Instagram 等(详见:redis.io/topics/whos-using-redis)使用。

你可以使用 Redis 做会话(session)处理程序,当你使用负载均衡的分布式服务时特别有用。Redis 也可以用作发布/订阅系统,很优雅的创建一个在线聊天或者实时订购程序。关于 Redis 的文档,所有的命令以及其它信息都能在项目网站 redis.io 上找到。

一直以来都有 Redis 和 Memcache 哪个更好的争论,文章 as the benchmarks show 显示在相同的基础操作上两者不相上下。Redis 比 Memcache 有更多的特性,比如内存存储、磁盘持久化、原子操作、事务以及不用记录每一次变化到磁盘上而是用服务端的数据结构来代替。

在这篇文章中将介绍如何使用 Predis 库所提供的一部分基础但很有用的 Redis 命令。

容易安装

Redis 容易安装,简明的安装说明发表在产品的下载页。从我个人经验来看,如果你运行在 Ubuntu 上而又没有安装 TCL(只需运行 sudo apt-get install tcl 即可安装)将会报错。一旦 Redis 安装完成,你可以运行服务:

Redis 的网站上显示的有很多语言可用的 Redis 客户端,每种语言都有好几个。对于 PHP 来说有 5 个。在本文中我使用的是 Predis 库,但是你可能也需要作为 PHP 模块编译、安装的 phpredis 扩展

译者注:有些人可能在 Predis 库和 phpredis 扩展中难于选择,但两者在一般场景下相差不大,目前都支持 PHP7。phpredis 扩展在性能上可能有一些优势,而 Predis 库源码更加优雅(适合学习、阅读),支持 PHP 新的语法特征,但文档较少(这也是我翻译这篇文章的主要原因)。还有,Predis 也有扩展支持(但作者好像没精力维护了,目前还不支持 PHP7),用于提高性能和提供一下其它的特性。

如果你向我一样在机器上安装的有 Git,你只需克隆 Predis 仓库。否则你就要下载 ZIP 包然后解压。(译者注:可以使用更加简便的 composer 安装:composer require predis/predis

测试一下,创建一个如下内容的 test.php 文件,测试是否能通过 Predis 连接上运行着的 Redis。

当你运行这个脚本,你应该能如愿的看见(输出)信息:”Successfully connected to Redis”。

译者注:这里可能有点小问题,代码不会按照预期捕获 Redis 连接失败的异常,因为在初始化 Redis 客户端类的时候并没有真正的连接,而是在运行第一个命令时才做连接,所以需要额外运行一下 $redis->connect(); 来触发连接操作。详见:https://github.com/nrk/predis/issues/61

使用 Redis

在这个章节你将了解众多 Redis 提供的命令的概况。Memcache 也有相似的命令,如果你熟悉 Memcache,这些列表对你来说看起来都不会陌生。 Continue Reading…

Nginx+PHP-FPM 500 504错误简析

最近新配置的 Nginx + PHP-FPM 环境遇到了几次 Nginx 500(Internal Server Error) 和 504(gateway timeout) 错误。就索性把配置修改的过程记录下来,已备后查。

先说一下 500 错误,这个一般是 PHP 代码错了,看一下 PHP 的 error 日志就清楚了。但有时候是 Nginx 和 PHP-FPM 配置不当导致的,比如说上传文件的场景。我们都知道 php.ini 文件里面 post_max_size 和 upload_max_filesize 两个配置项决定了文件上传的大小。但上调配置后发现大文件上传还是会 500,原来是因为 Nginx 也会限制上传文件的大小,有 client_max_body_size 配置项决定。这里还有一个比较重要的参数:client_body_buffer_size。为了节省资源 Nginx 不会把 request body 里面的数据一口气读到内存中,而是分片处理,这个参数就是设置缓存区的大小(一般也不用改),大于缓冲区大小的数据会以一个个小文件(buffer_size)形式存储到 client_body_temp_path,为了避免高并发下缓存文件冲突和一些文件系统限制(比如 ext3 磁盘格式文件夹下一级文件个数限制为 3.2w 个)以及提高性能可根据业务需要设置多级子目录。还需特别注意的是路径要有被 Nginx 进程写的权限,不然又会 500 错误了。

然后是 504 错误,除了业务代码本身超时更多是配置问题。还是先说 PHP 的配置,很多人都知道 PHP 脚本有执行时间限制(php.ini 中的 max_execution_time),但在 PHP-FPM 却是 php-fpm.conf 中的 request_terminate_timeout 配置项控制的。还有就是 max_input_time 配置项,值比较小的话在接收大文件或大数据量 POST 请求的时候可能会超时,一般不用做改动,因为大部分场景都是上游 web server 接收好数据后传给 PHP-FPM 进程的,传输的非常快,几乎不会超时,但也不能一概而论,要看具体业务场景。然后就是 Nginx 的锅了,刚也说到是 Nginx 接收好(或者说是转发)数据后传给下游,在这个承上启下的环节很容易超时。几个比较重要的配置项:fastcgi_send_timeout 控制向下游(这里是 PHP-FPM)发送数据的超时时间,与之对应的一个配置项是 fastcgi_read_timeout,表示读取下游 response 的超时时间,这里需要注意的是要和前面说的 request_terminate_timeout 保持一致(或大于)。还有一个配置项 fastcgi_connect_timeout,表示和下游 FastCGI 建立连接的超时时间,不过这个值就不用设置的太长了,不然下游某个节点挂了就会夯住 Nginx 的处理线程,影响业务。

我所遇到的场景就是上面这些,可能还有其它一些情况,具体问题具体分析应该也很好解决。

参考资料:

http://nginx.org/en/docs/http/ngx_http_core_module.html

http://nginx.org/en/docs/http/ngx_http_fastcgi_module.html

《PHP-CGI 进程 CPU 100% 与 file_get_contents 函数的关系》 http://zyan.cc/tags/request_terminate_timeout/1/

《PHP超时处理全面总结》 https://segmentfault.com/a/1190000000313184

《PHP file upload affected or not by max_input_time?》 http://stackoverflow.com/questions/11387113/php-file-upload-affected-or-not-by-max-input-time

一个 MySQL 用户名长度的坑

今天使用 PHP 连接一个 MySQL 数据库的时候连不上,提示无权限。

因为 MySQL 是在另外一个机房,首先想到的是防火墙的原因,但使用 MySQL-cli 却能正常连接,遂排除这种可能。

又怀疑是 PHP 框架的问题,写了一个简单的测试脚本,主要语句:mysqli_connect(),并打印出错误。运行一下还是不行,错误如下:

真是奇了怪了,这个用户创建时指定的是通配符 ‘%’,而且在别的机器都可以连接。又把测试脚本放在以前一直运行的环境(PHP 7)中,能运行通过。

看来是是环境问题(PHP 5.4)了,回过头来看那句报错,发现用户名好像被截断了(应该是xxx_user),是不是显示的问题,随便改一个用户名试试,同样报错,用户名却没有截断。这时又想了想是不是 MySQL 的版本太高(5.7.10)了。找了个 MySQL 5.5 的环境,创建相同的用户却发现报错了:String ‘xxx_user’ is too long for user name (should be no longer than 16)。查询 MySQL 文档发现:MySQL user names can be up to 32 characters long (16 characters before MySQL 5.7.8). http://dev.mysql.com/doc/refman/5.7/en/user-names.html

这样看来应该是老版 PHP 的 mysqli 扩展内部限定了用户名的长度,但新版的 MySQL 却可以创建更长的用户名了。知道原因了就很好办了,创建一个短用户名 OK 了。其实也是阴差阳错,因为新库是多应用共有,所以用户名创建的比较长。_(┐「ε:)_

PHP MySQL 持久连接(mysql_pconnect)

先来一段 PHP 连接 MySQL 的经典代码:

这没什么问题,一直这样用。后来看文档发现有个函数 mysql_pconnect(打开一个到 MySQL 服务器的持久连接)。官方文档是这样介绍的:

首先,当连接的时候本函数将先尝试寻找一个在同一个主机上用同样的用户名和密码已经打开的(持久)连接,如果找到,则返回此连接标识而不打开新连接。

其次,当脚本执行完毕后到 SQL 服务器的连接不会被关闭,此连接将保持打开以备以后使用(mysql_close() 不会关闭由 mysql_pconnect() 建立的连接)。

我们都知道建立 MySQL 连接比较消耗资源,要是能复用连接那不是牛逼了。但是 PHP 不是脚本语言吗?运行完了啥都没了,怎么维持持久连接呢?

实践是检验真理的唯一标准,还是试一试吧:

通过 URL 访问 pconnect.php,页面加载完成后,等待 10s ,新开浏览器再次访问。在 MySQL 命令行运行「show full processlist;」查看建立的连接:

2015-10-15_001413

感觉并没有复用呀,再次访问还是重新建立了连接(从 Time 字段的值可以看出),那么试试访问 connect.php :

2015-10-14_230252

很明显脚本执行完成连接就断了,这也是 mysql_connect 的特征,符合常理。但是怎么没有出现 mysql_pconnect 的特征呢?这种时候没办法,只能仔细再看文档,发现这么一句话:

注意,此种连接仅能用于模块版本的 PHP。

Continue Reading…

php中require和include的区别之我见

require和include在各种编程语言中基本都存在,都是用来引入外部文件,但两者却不易区分。

其实从两个语句的字面意思很容易区别开来。

require有“需求”的含义,意思就是说用require引入的文件是必须要引入的,如果找不到这个文件会报一个错误,不能继续执行后面的语句。

include有“包括”的含义,意思就是用include引入的文件只是简单包括,如果找不到这个文件只会产生一个警告信息,还是会继续执行后面的语句。

基于这点区别,两者的应用场景就很清楚了。如果你引入的文件是必须的则建议用require。使用include的话,若此文件不存在,虽然后面的语句会继续执行但因为缺少必要组件还是会报错,而且可能会出现一些莫名其妙、不可预料的错误。

关于require和include的却别我认为搞清楚这一点就可以了,其它的一些细微差别没必要去彻底弄清楚。把精力过多的放在这些地方有点本末倒置。

还有一个就是require/include和(require/include)_once间的区别,加不加”_once”唯一的区别就是如果该文件中已经被包含过,则不会再次包含。如同此语句名字(_once)暗示的那样,只会包含一次。如果包含两次,PHP 5 会发出致命错误因为函数已经被定义,但是 PHP 4 不会对在 return 之后定义的函数报错。

那是不是就推荐使用(require/include)_once呢?其实不然,检查一个文件是否已经被包含是需要一个过程的,会消耗系统资源,使用require/include效率更高。还有一个原因,使用(require/include)_once说明你对自己的代码不熟,没信心。而当一个项目需要大量使用(require/include)_once来保证不出错的话,说明这个项目的代码已经非常脆弱了,可能其它一点小错误就会造成整个系统的崩溃。

参考资料:风雪之隅《再一次, 不要使用(include/require)_once》