框架提供了快速應用程式開發的工具,但通常會隨著您創建功能的速度而增加技術債務。當可維護性不是開發人員有目的地關注的焦點時,就會產生技術債。由於缺乏單元測試和結構,未來的變更和調試成本高昂。
以下是如何開始建立程式碼以實現可測試性和可維護性 - 並節省您的時間。
讓我們從一些人為但典型的程式碼開始。這可能是任何給定框架中的模型類別。
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; } }
此程式碼可以工作,但需要改進:
$_SESSION
全域變數。單元測試框架(例如 PHPUnit)依賴命令列,其中 $_SESSION
和許多其他全域變數不可用。 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
物件。我們還需要一個可用的資料庫連線來進行測試。
為了讓這個單元測試運作,我們需要:
那麼,讓我們開始討論如何改進這一點。
在這個簡單的上下文中,檢索目前使用者的函數是不必要的。這是一個人為的範例,但本著 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 連線。我們告訴模擬物件期望呼叫 select
、where
、limit
和 get
# 方法。我返回 Mock 本身,以反映 SQL 連接物件如何返回自身 ($this
),從而使其方法呼叫「可連結」。請注意,對於 get
方法,我傳回資料庫呼叫結果 - 填入了使用者資料的 stdClass
物件。
這解決了一些問題:
我们还可以做得更好。这就是它变得有趣的地方。
为了进一步改进这一点,我们可以定义并实现一个接口。考虑以下代码。
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); } }
这里发生了一些事情。
addUser()
方法。User
模型中强制使用实现 UserInterface
的类。这样可以保证数据源始终有一个可用的 getUser()
方法,无论使用哪个数据源来实现 UserInterface
。请注意,我们的
User
对象类型提示UserInterface
在其构造函数中。这意味着实现UserInterface
的类必须传递到User
对象中。这是我们所依赖的保证 - 我们需要getUser
方法始终可用。
这样做的结果是什么?
User
类,我们可以轻松地模拟数据源。 (测试数据源的实现将是单独的单元测试的工作)。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
模型的创建移至应用程序配置中的一个位置。结果是:
User
对象和选择的数据存储在我们应用程序的一个位置定义。User
模型从使用 MySQL 切换到 ONE 位置中的任何其他数据源。这更易于维护。在本教程的过程中,我们完成了以下任务:
我相信您已经注意到,我们以可维护性和可测试性的名义添加了更多代码。可以对这种实现提出强有力的论据:我们正在增加复杂性。事实上,这需要项目的主要作者和合作者对代码有更深入的了解。
但是,技术债务总体减少远远超过了解释和理解的成本。
您可以使用 Composer 轻松地将 Mockery 和 PHPUnit 包含到您的应用程序中。将这些添加到 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 的更多信息。
对于 PHP,请考虑使用 Laravel 4,因为它特别利用了容器和此处介绍的其他概念。
感谢您的阅读!
以上是建立可測試且可維護的 PHP 程式碼的詳細內容。更多資訊請關注PHP中文網其他相關文章!