首頁  >  文章  >  後端開發  >  建立可測試且可維護的 PHP 程式碼

建立可測試且可維護的 PHP 程式碼

王林
王林原創
2023-08-29 22:01:08818瀏覽

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

框架提供了快速應用程式開發的工具,但通常會隨著您創建功能的速度而增加技術債務。當可維護性不是開發人員有目的地關注的焦點時,就會產生技術債。由於缺乏單元測試和結構,未來的變更和調試成本高昂。

以下是如何開始建立程式碼以實現可測試性和可維護性 - 並節省您的時間。


#我們將(鬆散地)覆蓋

  1. 乾燥
  2. 依賴注入
  3. 介面
  4. 容器
  5. 使用 PHPUnit 進行單元測試

讓我們從一些人為但典型的程式碼開始。這可能是任何給定框架中的模型類別。

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,並測試該呼叫的結果。我們還可以切換單獨的資料庫連接(假設兩者都實現相同的檢索資料的方法)。酷。

讓我們看看單元測試可能會是什麼樣子。

<?php

use Mockery as m;
use Fideloper\User;

class SecondUserTest extends PHPUnit_Framework_TestCase {

    public function testGetCurrentUserMock()
    {
        $db_connection = $this->_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 物件。

這解決了一些問題:

  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 模型中。

我们还简化了单元测试!

<?php

use Mockery as m;
use Fideloper\User;

class ThirdUserTest extends PHPUnit_Framework_TestCase {

    public function testGetCurrentUserMock()
    {
        $userRepo = $this->_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中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn