0

0

创建可测试和可维护的 PHP 代码

王林

王林

发布时间:2023-08-29 22:01:08

|

939人浏览过

|

来源于php中文网

原创

创建可测试和可维护的 php 代码

框架提供了快速应用程序开发的工具,但通常会随着您创建功能的速度而增加技术债务。当可维护性不是开发人员有目的地关注的焦点时,就会产生技术债务。由于缺乏单元测试和结构,未来的更改和调试成本高昂。

以下是如何开始构建代码以实现可测试性和可维护性 - 并节省您的时间。


我们将(松散地)覆盖

  1. 干燥
  2. 依赖注入
  3. 接口
  4. 容器
  5. 使用 PHPUnit 进行单元测试

让我们从一些人为但典型的代码开始。这可能是任何给定框架中的模型类。

立即学习PHP免费学习笔记(深入)”;

class User {

public function getCurrentUser()
{
    $user_id = $_SESSION['user_id'];

    $user = App::db->select('id, username')
                    ->where('id', $user_id)
                    ->limit(1)
                    ->get();

    if ( $user->num_results() > 0 )
    {
            return $user->row();
    }

    return false;
}

}

此代码可以工作,但需要改进:

  1. 这是不可测试的。
    • 我们依赖 $_SESSION 全局变量。单元测试框架(例如 PHPUnit)依赖于命令行,其中 $_SESSION 和许多其他全局变量不可用。
    • 我们依赖数据库连接。理想情况下,在单元测试中应避免实际的数据库连接。测试是关于代码,而不是数据。
  2. 此代码的可维护性并不理想。例如,如果我们更改数据源,则需要更改应用程序中使用的每个 App::db 实例中的数据库代码。另外,如果我们不想要当前用户的信息怎么办?

尝试的单元测试

这里尝试为上述功能创建单元测试。

class UserModelTest extends PHPUnit_Framework_TestCase {

    public function testGetUser()
    {
        $user = new User();

        $currentUser = $user->getCurrentUser();

        $this->assertEquals(1, $currentUser->id);
    }

}

让我们检查一下。首先,测试将失败。 User 对象中使用的 $_SESSION 变量在单元测试中不存在,因为它在命令行中运行 PHP。

其次,没有数据库连接设置。这意味着,为了完成这项工作,我们需要引导我们的应用程序以获得 App 对象及其 db 对象。我们还需要一个可用的数据库连接来进行测试。

为了使这个单元测试工作,我们需要:

  1. 为我们的应用程序中运行的 CLI (PHPUnit) 设置配置
  2. 依赖数据库连接。这样做意味着依赖与我们的单元测试分开的数据源。如果我们的测试数据库没有我们期望的数据怎么办?如果我们的数据库连接很慢怎么办?
  3. 依赖引导的应用程序会增加测试的开销,从而显着减慢单元测试的速度。理想情况下,我们的大多数代码都可以独立于所使用的框架进行测试。

那么,让我们开始讨论如何改进这一点。


保持代码干燥

在这个简单的上下文中,检索当前用户的函数是不必要的。这是一个人为的示例,但本着 DRY 原则的精神,我选择进行的第一个优化是概括此方法。

class User {

    public function getUser($user_id)
    {
        $user = App::db->select('user')
                        ->where('id', $user_id)
                        ->limit(1)
                        ->get();

        if ( $user->num_results() > 0 )
        {
            return $user->row();
        }

        return false;
    }

}

这提供了一种我们可以在整个应用程序中使用的方法。我们可以在调用时传入当前用户,而不是将该功能传递给模型。当代码不依赖其他功能(例如会话全局变量)时,代码更加模块化和可维护。

但是,这仍然无法按预期进行测试和维护。我们仍然依赖数据库连接。


依赖注入

让我们通过添加一些依赖注入来帮助改善这种情况。当我们将数据库连接传递到类中时,我们的模型可能如下所示。

class User {

    protected $_db;

    public function __construct($db_connection)
    {
        $this->_db = $db_connection;
    }

    public function getUser($user_id)
    {
        $user = $this->_db->select('user')
                        ->where('id', $user_id)
                        ->limit(1)
                        ->get();

        if ( $user->num_results() > 0 )
        {
            return $user->row();
        }

        return false;
    }

}

现在,我们的 User 模型的依赖项已经提供了。我们的类不再假定某个数据库连接,也不再依赖于任何全局对象。

至此,我们的类就基本可以测试了。我们可以传入我们选择的数据源(大部分)和用户 ID,并测试该调用的结果。我们还可以切换单独的数据库连接(假设两者都实现相同的检索数据的方法)。酷。

让我们看看单元测试可能是什么样子。

_mockDb();

        $user = new User( $db_connection );

        $result = $user->getUser( 1 );

        $expected = new StdClass();
        $expected->id = 1;
        $expected->username = 'fideloper';

        $this->assertEquals( $result->id, $expected->id, 'User ID set correctly' );
        $this->assertEquals( $result->username, $expected->username, 'Username set correctly' );
    }

    protected function _mockDb()
    {
        // "Mock" (stub) database row result object
        $returnResult = new StdClass();
        $returnResult->id = 1;
        $returnResult->username = 'fideloper';

        // Mock database result object
        $result = m::mock('DbResult');
        $result->shouldReceive('num_results')->once()->andReturn( 1 );
        $result->shouldReceive('row')->once()->andReturn( $returnResult );

        // Mock database connection object
        $db = m::mock('DbConnection');

        $db->shouldReceive('select')->once()->andReturn( $db );
        $db->shouldReceive('where')->once()->andReturn( $db );
        $db->shouldReceive('limit')->once()->andReturn( $db );
        $db->shouldReceive('get')->once()->andReturn( $result );

        return $db;
    }

}

我在此单元测试中添加了一些新内容:Mockery。 Mockery 允许您“模拟”(伪造)PHP 对象。在本例中,我们正在模拟数据库连接。通过我们的模拟,我们可以跳过测试数据库连接并简单地测试我们的模型。

想要了解有关 Mockery 的更多信息吗?

在本例中,我们正在模拟 SQL 连接。我们告诉模拟对象期望调用 selectwherelimitget 方法。我返回 Mock 本身,以反映 SQL 连接对象如何返回自身 ($this),从而使其方法调用“可链接”。请注意,对于 get 方法,我返回数据库调用结果 - 填充了用户数据的 stdClass 对象。

这解决了一些问题:

MVM mall 网上购物系统
MVM mall 网上购物系统

采用 php+mysql 数据库方式运行的强大网上商店系统,执行效率高速度快,支持多语言,模板和代码分离,轻松创建属于自己的个性化用户界面 v3.5更新: 1).进一步静态化了活动商品. 2).提供了一些重要UFT-8转换文件 3).修复了除了网银在线支付其它支付显示错误的问题. 4).修改了LOGO广告管理,增加LOGO链接后主页LOGO路径错误的问题 5).修改了公告无法发布的问题,可能是打压

下载
  1. 我们仅测试我们的模型类。我们还没有测试数据库连接。
  2. 我们能够控制模拟数据库连接的输入和输出,因此可以可靠地测试数据库调用的结果。我知道由于模拟数据库调用,我将获得用户 ID“1”。
  3. 我们不需要引导我们的应用程序,也不需要提供任何配置或数据库来进行测试。

我们还可以做得更好。这就是它变得有趣的地方。


接口

为了进一步改进这一点,我们可以定义并实现一个接口。考虑以下代码。

interface UserRepositoryInterface {
    public function getUser($user_id);
}

class MysqlUserRepository implements UserRepositoryInterface {

    protected $_db;

    public function __construct($db_conn)
    {
        $this->_db = $db_conn;
    }

    public function getUser($user_id)
    {
        $user = $this->_db->select('user')
                    ->where('id', $user_id)
                    ->limit(1)
                    ->get();

        if ( $user->num_results() > 0 )
        {
            return $user->row();
        }

        return false;
    }

}

class User {

    protected $userStore;

    public function __construct(UserRepositoryInterface $user)
    {
        $this->userStore = $user;
    }

    public function getUser($user_id)
    {
        return $this->userStore->getUser($user_id);
    }

}

这里发生了一些事情。

  1. 首先,我们为用户数据源定义一个接口。这定义了 addUser() 方法。
  2. 接下来,我们实现该接口。在本例中,我们创建一个 MySQL 实现。我们接受一个数据库连接对象,并使用它从数据库中获取用户。
  3. 最后,我们在 User 模型中强制使用实现 UserInterface 的类。这样可以保证数据源始终有一个可用的 getUser() 方法,无论使用哪个数据源来实现 UserInterface

请注意,我们的 User 对象类型提示 UserInterface 在其构造函数中。这意味着实现 UserInterface 的类必须传递到 User 对象中。这是我们所依赖的保证 - 我们需要 getUser 方法始终可用。

这样做的结果是什么?

  • 我们的代码现在完全可测试。对于User类,我们可以轻松地模拟数据源。 (测试数据源的实现将是单独的单元测试的工作)。
  • 我们的代码更加易于维护。我们可以切换不同的数据源,而无需更改整个应用程序的代码。
  • 我们可以创建任何数据源。 ArrayUser、MongoDbUser、CouchDbUser、MemoryUser 等
  • 如果需要,我们可以轻松地将任何数据源传递到我们的 User 对象。如果您决定放弃 SQL,则只需创建一个不同的实现(例如 MongoDbUser)并将其传递到您的 User 模型中。

我们还简化了单元测试!

_mockUserRepo();

        $user = new User( $userRepo );

        $result = $user->getUser( 1 );

        $expected = new StdClass();
        $expected->id = 1;
        $expected->username = 'fideloper';

        $this->assertEquals( $result->id, $expected->id, 'User ID set correctly' );
        $this->assertEquals( $result->username, $expected->username, 'Username set correctly' );
    }

    protected function _mockUserRepo()
    {
        // Mock expected result
        $result = new StdClass();
        $result->id = 1;
        $result->username = 'fideloper';

        // Mock any user repository
        $userRepo = m::mock('Fideloper\Third\Repository\UserRepositoryInterface');
        $userRepo->shouldReceive('getUser')->once()->andReturn( $result );

        return $userRepo;
    }

}

我们已经完全取消了模拟数据库连接的工作。相反,我们只是模拟数据源,并告诉它当调用 getUser 时要做什么。

但是,我们仍然可以做得更好!


容器

考虑我们当前代码的用法:

// In some controller
$user = new User( new MysqlUser( App:db->getConnection("mysql") ) );
$user->id = App::session("user->id");

$currentUser = $user->getUser($user_id);

我们的最后一步是引入容器。容器。在上面的代码中,我们需要创建并使用一堆对象来获取当前用户。此代码可能散布在您的应用程序中。如果您需要从 MySQL 切换到 MongoDB,您仍然需要编辑上述代码出现的每个位置。那几乎不是干的。容器可以解决这个问题。

容器只是“包含”一个对象或功能。它类似于应用程序中的注册表。我们可以使用容器自动实例化一个新的 User 对象以及所有需要的依赖项。下面,我使用 Pimple,一个流行的容器类。

// Somewhere in a configuration file
$container = new Pimple();
$container["user"] = function() {
    return new User( new MysqlUser( App:db->getConnection('mysql') ) );
}

// Now, in all of our controllers, we can simply write:
$currentUser = $container['user']->getUser( App::session('user_id') );

我已将 User 模型的创建移至应用程序配置中的一个位置。结果是:

  1. 我们的代码保持干燥。 User 对象和选择的数据存储在我们应用程序的一个位置定义。
  2. 我们可以将 User 模型从使用 MySQL 切换到 ONE 位置中的任何其他数据源。这更易于维护。

最终想法

在本教程的过程中,我们完成了以下任务:

  1. 保持我们的代码干燥且可重用
  2. 创建了可维护的代码 - 如果需要,我们可以在整个应用程序的一个位置切换对象的数据源
  3. 使我们的代码可测试 - 我们可以轻松模拟对象,而无需依赖引导我们的应用程序或创建测试数据库
  4. 了解如何使用依赖注入和接口来创建可测试和可维护的代码
  5. 了解容器如何帮助我们的应用程序更易于维护

我相信您已经注意到,我们以可维护性和可测试性的名义添加了更多代码。可以对这种实现提出强有力的论据:我们正在增加复杂性。事实上,这需要项目的主要作者和合作者对代码有更深入的了解。

但是,技术债务总体减少远远超过了解释和理解的成本。

  • 代码的可维护性大大提高,可以在一个位置而不是多个位置进行更改。
  • 能够(快速)进行单元测试将大幅减少代码中的错误 - 特别是在长期或社区驱动的(开源)项目中。
  • 提前做额外的工作节省时间并减少以后的麻烦。

资源

您可以使用 Composer 轻松地将 MockeryPHPUnit 包含到您的应用程序中。将这些添加到 composer.json 文件中的“require-dev”部分:

"require-dev": {
    "mockery/mockery": "0.8.*",
    "phpunit/phpunit": "3.7.*"
}

然后,您可以按照“dev”要求安装基于 Composer 的依赖项:

$ php composer.phar install --dev

在 Nettuts+ 上了解有关 Mockery、Composer 和 PHPUnit 的更多信息。

  • 嘲笑:更好的方法
  • 使用 Composer 轻松进行包管理
  • 测试驱动的 PHP

对于 PHP,请考虑使用 Laravel 4,因为它特别利用了容器和此处介绍的其他概念。

感谢您的阅读!

PHP速学教程(入门到精通)
PHP速学教程(入门到精通)

PHP怎么学习?PHP怎么入门?PHP在哪学?PHP怎么学才快?不用担心,这里为大家提供了PHP速学教程(入门到精通),有需要的小伙伴保存下载就能学习啦!

下载

本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
Java空对象相关教程合集
Java空对象相关教程合集

本专题整合了Java空对象相关教程,阅读专题下面的文章了解更多详细内容。

0

2026.01.29

clawdbot ai使用教程 保姆级clawdbot部署安装手册
clawdbot ai使用教程 保姆级clawdbot部署安装手册

Clawdbot是一个“有灵魂”的AI助手,可以帮用户清空收件箱、发送电子邮件、管理日历、办理航班值机等等,并且可以接入用户常用的任何聊天APP,所有的操作均可通过WhatsApp、Telegram等平台完成,用户只需通过对话,就能操控设备自动执行各类任务。

19

2026.01.29

clawdbot龙虾机器人官网入口 clawdbot ai官方网站地址
clawdbot龙虾机器人官网入口 clawdbot ai官方网站地址

clawdbot龙虾机器人官网入口:https://clawd.bot/,clawdbot ai是一个“有灵魂”的AI助手,可以帮用户清空收件箱、发送电子邮件、管理日历、办理航班值机等等,并且可以接入用户常用的任何聊天APP,所有的操作均可通过WhatsApp、Telegram等平台完成,用户只需通过对话,就能操控设备自动执行各类任务。

16

2026.01.29

Golang 网络安全与加密实战
Golang 网络安全与加密实战

本专题系统讲解 Golang 在网络安全与加密技术中的应用,包括对称加密与非对称加密(AES、RSA)、哈希与数字签名、JWT身份认证、SSL/TLS 安全通信、常见网络攻击防范(如SQL注入、XSS、CSRF)及其防护措施。通过实战案例,帮助学习者掌握 如何使用 Go 语言保障网络通信的安全性,保护用户数据与隐私。

8

2026.01.29

俄罗斯Yandex引擎入口
俄罗斯Yandex引擎入口

2026年俄罗斯Yandex搜索引擎最新入口汇总,涵盖免登录、多语言支持、无广告视频播放及本地化服务等核心功能。阅读专题下面的文章了解更多详细内容。

567

2026.01.28

包子漫画在线官方入口大全
包子漫画在线官方入口大全

本合集汇总了包子漫画2026最新官方在线观看入口,涵盖备用域名、正版无广告链接及多端适配地址,助你畅享12700+高清漫画资源。阅读专题下面的文章了解更多详细内容。

210

2026.01.28

ao3中文版官网地址大全
ao3中文版官网地址大全

AO3最新中文版官网入口合集,汇总2026年主站及国内优化镜像链接,支持简体中文界面、无广告阅读与多设备同步。阅读专题下面的文章了解更多详细内容。

350

2026.01.28

php怎么写接口教程
php怎么写接口教程

本合集涵盖PHP接口开发基础、RESTful API设计、数据交互与安全处理等实用教程,助你快速掌握PHP接口编写技巧。阅读专题下面的文章了解更多详细内容。

11

2026.01.28

php中文乱码如何解决
php中文乱码如何解决

本文整理了php中文乱码如何解决及解决方法,阅读节专题下面的文章了解更多详细内容。

16

2026.01.28

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号