随着 PHP 从一种简单的脚本语言转变为一种成熟的编程语言,一个典型的 PHP 应用程序的代码库的复杂性也随之增大。为了控制对这些应用程序的支持和维护,我们可以使用各种测试工具来自动化该流程。其中一种是单元测试,它允许您直接测试所编写代码的正确性。然而,通常遗留代码库是不适合进行这种测试的。本文将介绍对包含常见问题的 PHP 代码的重构策略,以便简化使用流行的单元测试工具进行测试的过程,同时减少改进代码库的依赖性。

简介

回顾 PHP 15 年的发展历程,我们发现它已经从一个简单的用来替代当时流行的 CGI 脚本的动态脚本语言变成一种成熟的现代编程语言。 随着代码库的增长,手动测试已经变成不可能完成的任务,无论是大是小,所有代码的变化都会对整个应用程序产生影响。这些影响可能小到只是影响某个页面的加 载或表单保存,也可能是产生难以检测的问题,或者产生只在特定条件下才会出现的错误。甚至,它可能会使以前修复的问题重新出现在应用程序中。为此开发了许 多测试工具来解决这些问题。

其中一种流行的方法是所谓的功能或验收测试,它会通过应用程序的典型用户交互来测试这个应用程序。这是一种 很适合测试应用程序中各个进程的方法,但是测试过程可能非常慢,而且一般无法测试底层的类和方法是否按要求正常工作。这时,我们需要使用另一种测试方法, 那就是单元测试。单元测试的目标是测试应用程序底层代码的功能,保证它们执行后产生正确的结果。通常,这些 “不断增大” 的 Web 应用程序会慢慢出现越来越多久而久之难以测试的遗留代码,这使开发团队很难保证应用程序测试的覆盖率。这通常被称为 “不可测试代码”。现在让我们看看如何识别应用程序中的不可测试代码,以及修复这些代码的方法。

识别不可测试的代码

关于代码库不可测试性的问题域通常在编写代码时是不明显的。当编写 PHP 应用程序代码时,人们倾向于按照 Web 请求的流程来编写代码,这通常就是在应用程序设计时采用一种更加流程化的方法。急于完成项目或快速修复应用程序都可能促使开发人员 “走捷径”,以便快速完成编码。以前,编写不当或者混乱的代码可能会加重应用程序中的不可测试性问题,因为开发人员通常会进行风险最小的修复,即使它可能产生后续的支持问题。这些问题域都是无法通过一般的单元测试发现的。

依赖全局状态的函数

全局变量在 PHP 应用程序中很方便。它们允许您在应用程序中初始化一些变量或对象,然后在应用程序的其他位置使用。然而,这种灵活性是有代价的,过度使用全局变量是不可测试代码的一个通病。我们可以在 清单 1中看到这种情况。

清单 1. 依赖于全局状态的函数
 <?php
function formatNumber($number)
{
global $decimal_precision, $decimal_separator, $thousands_separator; if ( !isset($decimal_precision) ) $decimal_precision = 2;
if ( !isset($decimal_separator) ) $decimal_separator = '.';
if ( !isset($thousands_separator) ) $thousands_separator = ','; return number_format($number, $decimal_precision, $decimal_separator,
$thousands_separator);
}

这些全局变量带来了两个不同的问题。第一个问题是您需要在测试中考虑所有这些全局变量,保证给它们设置了函数可接受的有效值。第二个问题更为严重, 那就是您无法修改后续测试的状态并使它们的结果无效,您需要保证将全局状态重置为测试运行之前的状态。PHPUnit 有一些工具可以帮您备份全局变量并在测试运行后恢复它们的值,这些工具能够帮助解决这个问题。然而,更好的方法是使测试类能够直接给方法传入这些全局变量的值。清单 2显示了采用这种方法的一个例子。

清单 2. 修改这个函数以支持重写全局变量
 <?php
function formatNumber($number, $decimal_precision = null, $decimal_separator = null,
$thousands_separator = null)
{
if ( is_null($decimal_precision) ) global $decimal_precision;
if ( is_null($decimal_separator) ) global $decimal_separator;
if ( is_null($thousands_separator) ) global $thousands_separator; if ( !isset($decimal_precision) ) $decimal_precision = 2;
if ( !isset($decimal_separator) ) $decimal_separator = '.';
if ( !isset($thousands_separator) ) $thousands_separator = ','; return number_format($number, $decimal_precision, $decimal_separator,
$thousands_separator);
}

这样做不仅使代码变得更具可测试性,而且也使它不依赖于方法的全局变量。这使得我们能够对代码进行重构,不再使用全局变量。

无法重置的单一实例

单一实例指的是旨在让应用程序中一次只存在一个实例的类。它们是应用程序中用于全局对象的一种常见模式,如数据库连接和配置设置。它们通常被认为是应用程序的禁忌, 因为许多开发人员认为创建一个总是可用的对象用处不大,因此他们并不太注意这一点。这个问题主要源于单一实例的过度使用,因为它会造成大量不可扩展的所谓 god objects 的出现。但是从测试的角度看,最大的问题是它们通常是不可更改的。清单 3就是这样一个例子。

清单 3. 我们要测试的 Singleton 对象
 <?php
class Singleton
{
private static $instance; protected function __construct() { }
private final function __clone() {} public static function getInstance()
{
if ( !isset(self::$instance) ) {
self::$instance = new Singleton;
} return self::$instance;
}
}

您可以看到,当单一实例首次实例化之后,每次调用 getInstance() 方法实际上返回的都是同一个对象,它不会创建新的对象,如果我们对这个对象进行修改,那么就可能造成很严重的问题。最简单的解决方案就是给对象增加一个 reset 方法。清单 4 显示的就是这样一个例子。

清单 4. 增加了 reset 方法的 Singleton 对象
 <?php
class Singleton
{
private static $instance; protected function __construct() { }
private final function __clone() {} public static function getInstance()
{
if ( !isset(self::$instance) ) {
self::$instance = new Singleton;
} return self::$instance;
} public static function reset()
{
self::$instance = null;
}
}

现在,我们可以在每次测试之前调用 reset 方法,保证我们在每次测试过程中都会先执行 singleton 对象的初始化代码。总之,在应用程序中增加这个方法是很有用的,因为我们现在可以轻松地修改单一实例。

使用类构造函数

进行单元测试的一个良好做法是只测试需要测试的代码,避免创建不必要的对象和变量。您创建的每一个对象和变量都需要在测试之后删除。这对于文件和数据库表等 麻烦的项目来说成为一个问题,因为在这些情况下,如果您需要修改状态,那么您必须更小心地在测试完成之后进行一些清理操作。坚持这一规则的最大障碍在于对 象本身的构造函数,它执行的所有操作都是与测试无关的。清单 5 就是这样一个例子。

清单 5. 具有一个大 singleton 方法的类
 <?php
class MyClass
{
protected $results; public function __construct()
{
$dbconn = new DatabaseConnection('localhost','user','password');
$this->results = $dbconn->query('select name from mytable');
} public function getFirstResult()
{
return $this->results[0];
}
}

在这里,为了测试对象的 fdfdfd 方法,我们最终需要建立一个数据库连接,给表添加一些记录,然后在测试之后清除所有这些资源。如果测试 fdfdfd完全不需要这些东西,那么这个过程可能太过于复杂。因此,我们要修改 清单 6所示的构造函数。

清单 6. 为忽略所有不必要的初始化逻辑而修改的类
 <?php
class MyClass
{
protected $results; public function __construct($init = true)
{
if ( $init ) $this->init();
} public function init()
{
$dbconn = new DatabaseConnection('localhost','user','password');
$this->results = $dbconn->query('select name from mytable');
} public function getFirstResult()
{
return $this->results[0];
}
}

我们重构了构造函数中大量的代码,将它们移到一个 init() 方法中,这个方法默认情况下仍然会被构造函数调用,以避免破坏现有代码的逻辑。然而,现在我们在测试过程中只能够传递一个布尔值 false 给构造函数,以避免调用 init()方法和所有不必要的初始化逻辑。类的这种重构也会改进代码,因为我们将初始化逻辑从对象的构造函数分离出来了。

经硬编码的类依赖性

正如我们在前一节介绍的,造成测试困难的大量类设计问题都集中在初始化各种不需要测试的对象上。在前面,我们知道繁重的初始化逻 辑可能会给测试的编写造成很大的负担(特别是当测试完全不需要这些对象时),但是如果我们在测试的类方法中直接创建这些对象,又可能造成另一个问题。清单 7显示的就是可能造成这个问题的示例代码。

清单 7. 在一个方法中直接初始化另一个对象的类
 <?php
class MyUserClass
{
public function getUserList()
{
$dbconn = new DatabaseConnection('localhost','user','password');
$results = $dbconn->query('select name from user'); sort($results); return $results;
}
}

假设我们正在测试上面的 getUserList方法,但是我们的测试关注点是保证返回的 用户清单是按字母顺序正确排序的。在这种情况下,我们的问题不在于是否能够从数据库获取这些记录,因为我们想要测试的是我们是否能够对返回的记录进行排 序。问题是,由于我们是在这个方法中直接实例化一个数据库连接对象,所以我们需要执行所有这些繁琐的操作才能够完成方法的测试。因此,我们要对方法进行修 改,使这个对象可以在中间插入,如 清单 8所示。

清单 8. 这个类有一个方法会直接实例化另一个对象,但是也提供了一种重写的方法
 <?php
class MyUserClass
{
public function getUserList($dbconn = null)
{
if ( !isset($dbconn) || !( $dbconn instanceOf DatabaseConnection ) ) {
$dbconn = new DatabaseConnection('localhost','user','password');
}
$results = $dbconn->query('select name from user'); sort($results); return $results;
}
}

现在您可以直接传入一个对象,它与预期数据库连接对象相兼容,然后直接使用这个对象,而非创建一个新对象。您也可以传 入一个模拟对象,也就是我们在一些调用方法中,用硬编码的方式直接返回我们想要的值。在这里,我们可以模拟数据库连接对象的查询方法,这样我们就只需要返 回结果,而不需要真正地去查询数据库。进行这样的重构也能够改进这个方法,因为它允许您的应用程序在需要时插入不同的数据库连接,而不是只绑定一个指定的 默认数据库连接。

可测试代码的好处

显然,编写更具可测试性的代码肯定能够简化 PHP 应用程序的单元测试(正如您在本文展示的例子中所看到的),但是在这个过程中,它也能够改进应用程序的设计、模块化和稳定性。我们都曾经看到过 “spaghetti” 代码,它们在 PHP 应用程序的一个主要流程中充斥了大量的业务和表现逻辑,这毫无疑问会给那些使用这个应用程序的人造成严重的支持问题。在使代码变得更具可测试性的过程中, 我们对前面一些有问题的代码进行了重构;这些代码不仅设计上有问题,功能上也有问题。通过使这些函数和类的用途更广泛,以及通过删除硬编码的依赖性,我们 使之更容易被应用程序其他部分重用,我们提高了代码的可重用性。此外,我们还将编写不当的代码替换成更优质的代码,从而简化将来对代码库的支持。

结束语

在本文中,通过 PHP 应用程序中一些典型的不可测试代码示例,我们了解了如何改进 PHP 代码的可测试性。我们还介绍了这些情况是如何出现在应用程序中的,然后介绍了如何恰当地修复这些问题代码来便于进行测试。我们还了解了这些代码的修改不仅 能够提高代码的可测试性,也能够普遍改进代码的质量,以及提高重构代码的可重用性。

谈一谈PHP的代码重构的更多相关文章

  1. 浅谈Android保护技术__代码混淆

    浅谈Android保护技术__代码混淆   代码混淆 代码混淆(Obfuscated code)亦称花指令,是将计算机程序的代码,转换成一种功能上等价,但是难于阅读和理解的形式的行为.将代码中的各种元 ...

  2. CSS代码重构与优化之路

    作者:@狼狼的蓝胖子 网址:http://www.cnblogs.com/lrzw32/p/5100745.html 写CSS的同学们往往会体会到,随着项目规模的增加,项目中的CSS代码也会越来越多, ...

  3. CSS代码重构

    CSS代码重构的目的 我们写CSS代码时,不仅仅只是完成页面设计的效果,还应该让CSS代码易于管理,维护.我们对CSS代码重构主要有两个目的:1.提高代码性能2.提高代码的可维护性 提高代码性能 提高 ...

  4. 代码重构方向原则指导(转载 cnblogs)

    英文原文:Hill Climbing (Wonkish)   重构是一种对软件进行修改的行为,但它并不改变软件的功能特征,而是通过让软件程序更清晰,更简洁和更条理来改进软件的质量.代码重构之于软件,相 ...

  5. CSS代码重构与优化

    CSS代码重构的基本方法 前面说到了CSS代码重构的目的,现在我们来说说一些如何达到这些目的的一些基本方法,这些方法都是易于理解,容易实施的一些手段,大家平时可能也不知不觉地在使用它. 提高CSS性能 ...

  6. CSS代码重构与优化之路(转)

    CSS代码重构与优化之路   阅读目录 CSS代码重构的目的 CSS代码重构的基本方法 CSS方法论 我自己总结的方法 写CSS的同学们往往会体会到,随着项目规模的增加,项目中的CSS代码也会越来越多 ...

  7. 谈一谈Java8的函数式编程(二) --Java8中的流

    流与集合    众所周知,日常开发与操作中涉及到集合的操作相当频繁,而java中对于集合的操作又是相当麻烦.这里你可能就有疑问了,我感觉平常开发的时候操作集合时不麻烦呀?那下面我们从一个例子说起. 计 ...

  8. 从一张图开始,谈一谈.NET Core和前后端技术的演进之路

    从一张图开始,谈一谈.NET Core和前后端技术的演进之路 邹溪源,李文强,来自长沙.NET技术社区 一张图 2019年3月10日,在长沙.NET 技术社区组织的技术沙龙<.NET Core和 ...

  9. 谈一谈对MySQL InnoDB的认识及数据库事物处理的隔离级别

    介绍: InnoDB引擎是MySQL数据库的一个重要的存储引擎,和其他存储引擎相比,InnoDB引擎的优点是支持兼容ACID的事务(类似于PostgreSQL),以及参数完整性(有外键)等.现在Inn ...

  10. 谈一谈C#的事件

    谈一谈C#的事件 C#中事件基于委托,要理解事件要先理解委托,如果觉得自己关于委托不是很了解可以看看我前面写委托的文章 事件基于委托,是一种功能受限的委托,为委托提供了一种发布/订阅机制 使用委托时, ...

随机推荐

  1. jquery 让select元素中的某个option被选中

    jquery 操作select 取值,设置选中值 博客分类: javaScript selecttextvalue取值设置选中值 比如 <select class="type" ...

  2. Linux/UNIX脚本中 $0,$1,$2的含义

    linux中有三种标准输入输出,分别是STDIN,STDOUT,STDERR,对应的数字是0,1,2.   STDIN就是标准输入,默认从键盘读取信息: STDOUT是标准输出,默认将输出结果输出至终 ...

  3. 去除html标签 正则表达式

    /// <summary>        /// 去除html标签        /// </summary>        public static string Clea ...

  4. apache的80端口分发

    打开 conf 文件夹,找到下面的 httpd.conf 更改Listen 后面的端口号为:80:默认就是80端口 去掉下面的相关注释: #LoadModule proxy_module module ...

  5. centos下网络配置方法(网关、dns、ip地址配置)

    本文介绍了centos网络配置的方法,centos网络配置主要包括dns.网关.IP地址: 1.IP地址配置: /etc/sysconfig/network-scripts/ifcfg-eth0 2. ...

  6. 【Windows 10 IoT - 3】Windows 10 RTM安装及新特性(树莓派 Pi2)

    在<[Window 10 IoT - 1]Window 10系统安装(树莓派 Pi2)>中,我们介绍了Windows 10 IoT预览版的安装,正式版Windows 10 IOT(OS版本 ...

  7. 仿花田:内部相亲网站 意中人(Asp.net MVC,Bootstrap2)

    起因: 那是七月份了,看见单身的同事在上花田网,当时觉得风格比较清新,还没有世纪佳缘等那些网站那么商业化,加上又看到了bootrstrap,于是就想做个demo出来玩玩.中间自己又在做其他的事情,和w ...

  8. libevent (二) 接收TCP连接

    libevent 接收TCP连接 Evconnlistener 机制为您提供了侦听和接受传入的 TCP 连接的方法.下面的函数全部包含在`<event2/listener.h>`中. ev ...

  9. JavaScript 函数惰性载入

    最近看JavaScript高级程序设计,大有收获,接下来几天写一下读书笔记.之前写了一篇Ajax初步理解的随笔,里面有个函数用来创建XmlHttpRequest对象,浏览器兼容性原因,写出的代码通过大 ...

  10. Facebook网络模拟测试工具ATC使用

    Facebook在其工程博客(原文)上宣布开源移动网络测试工具Augmented Traffic Control(ATC),我迅速试用了一番,非常不错,对手游或者其他APP的调试和测试都非常有帮助,介 ...