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);
}

这样做不仅使代码更具可测试性,而且不依赖于方法中的全局变量。 这可以为将来完全不使用全局变量而重构此代码提供可能性。

无法重置的单例

单例是旨在在应用程序中一次仅存在一个实例的类。 它们是用于应用程序中全局对象的常用模式,例如数据库连接和配置设置。 在应用程序中,它们通常被视为禁忌,许多开发人员认为它们是不值得的区别,因为拥有一个始终可用的对象供使用的有用性。 其中大部分来自过度使用单例,其中许多所谓的上帝对象可能无法扩展。 但是从测试的角度来看,一个大问题是它们通常是不可变的。 让我们以清单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()方法的每次调用都将返回相同的对象,而不是新的对象,如果我们对该对象进行更改,则可能会成为一个大问题。 最简单的解决方案是向对象添加一种可以重置它的方法。 清单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方法来启动每次测试运行,以确保我们在每次测试运行中都经过单例对象的初始化代码。 一般而言,使用此方法可以对应用程序有所帮​​助,因为现在单例变得容易可变。

在类构造器中工作

单元测试的一个好习惯是只精确地测试您想要的,而避免设置比您绝对需要更多的对象和变量。 您设置的每个对象和变量也是事实之后需要删除的对象和变量。 对于诸如文件和数据库表之类的更讨厌的项目,这将成为一个问题,在这些项目中,如果您需要修改状态,则在测试完成后必须格外小心地清理轨道。 保持该规则完整的最大障碍是对象本身的构造函数,它可以执行与测试无关的所有事情。 考虑下面的清单5作为示例。

清单5.具有大单例方法的类
<?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应用程序编写单元测试的过程(在本文中的示例中已经看到),但是它还创建了设计更好,模块化程度更高和更稳定的代码。申请过程中。 我们都已经看到过各种级别的“意大利面条”代码,它们将业务和显示逻辑紧密地交织在一起,成为PHP应用程序中的一个大程序混乱,这无疑会引起任何需要深入研究的人的支持噩梦。 在使代码可测试的过程中,我们重构了以前有问题的代码。 不仅在设计上有问题,而且在功能上也有问题。 我们通过消除硬编码的依赖关系,使函数和类减少了单一用途,并使应用程序的其他区域更易使用,从而提供了更好的代码重用选项。 此外,我们通过删除质量较差的代码并将其替换为质量更好的代码,使将来对代码库的支持更加容易。

结论

在本文中,我们通过一些在PHP应用程序中经典发现的不可测试代码的示例,着眼于使PHP代码更具可测试性。 我们探索了这种情况在应用程序中如何产生,然后看到了如何最好地修复有问题的代码以使测试成为可能。 我们还看到了如何对代码进行这些更改,不仅使代码更具可测试性,而且总体上提高了代码质量,并在重构的代码部分中促进了代码重用。


翻译自: https://www.ibm.com/developerworks/opensource/library/os-refactoringphp/index.html

php 重构代码

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值