問題描述
我正在使用 TDD 開發一個外掛,而我完全無法測試的一件事是鉤子。
我的意思是 OK,我可以測試鉤子回撥,但是如何測試鉤子是否實際觸發 (兩個自定義鉤子和 WordPress 預設鉤子)?我假設有些嘲弄會有所幫助,但是我根本找不到我失蹤的東西。
我安裝了測試套件與 WP-CLI 。根據 this answer,init 鉤應該觸發,但… 它不會; 程式碼也在 WordPress 裡面工作。
從我的理解,載入程式最後被載入,所以沒有觸發 init 是有意義的,所以剩下的問題是:我應該如何測試鉤子是否被觸發?
謝謝!
引導檔案如下所示:
$_tests_dir = getenv('WP_TESTS_DIR');
if ( !$_tests_dir ) $_tests_dir = '/tmp/wordpress-tests-lib';
require_once $_tests_dir . '/includes/functions.php';
function _manually_load_plugin() {
require dirname( __FILE__ ) . '/../includes/RegisterCustomPostType.php';
}
tests_add_filter( 'muplugins_loaded', '_manually_load_plugin' );
require $_tests_dir . '/includes/bootstrap.php';
測試檔案看起來像這樣:
class RegisterCustomPostType {
function __construct()
{
add_action( 'init', array( $this, 'register_post_type' ) );
}
public function register_post_type()
{
register_post_type( 'foo' );
}
}
測試本身:
class CustomPostTypes extends WP_UnitTestCase {
function test_custom_post_type_creation()
{
$this->assertTrue( post_type_exists( 'foo' ) );
}
}
謝謝!
最佳解決方案
隔離測試
開發外掛時,測試它的最佳方式是不載入 WordPress 環境。
如果您編寫的程式碼可以輕鬆測試,無需 WordPress,您的程式碼變得更好。
單元測試的每個元件都應該被隔離測試:當你測試一個類時,只需要測試那個特定的類,假設所有其他程式碼都是完美的。
這就是為什麼單元測試稱為”unit” 的原因。
作為一個額外的好處,沒有載入核心,你的測試將執行得更快。
避免建構函式中的鉤子
我可以給你的一個提示是避免在建構函式中掛鉤。這將使您的程式碼可以孤立地進行測試。
我們來看看 OP 中的測試程式碼:
class CustomPostTypes extends WP_UnitTestCase {
function test_custom_post_type_creation() {
$this->assertTrue( post_type_exists( 'foo' ) );
}
}
我們假設這個測試失敗了。誰是罪魁禍首?
-
鉤子根本沒有新增或不正確?
-
註冊帖子型別的方法根本沒有被呼叫或者是錯誤的引數?
-
WordPress 中有錯誤?
如何改善?
我們假設你的類程式碼是:
class RegisterCustomPostType {
function init() {
add_action( 'init', array( $this, 'register_post_type' ) );
}
public function register_post_type() {
register_post_type( 'foo' );
}
}
(注:我將參考該版本的其餘答案)
我寫這個類的方法允許你建立類的例項而不呼叫 add_action 。
在上面的課上有兩件事要測試:
-
方法
init實際上呼叫add_action傳遞給它正確的引數 -
方法
register_post_type實際呼叫register_post_type函式
我沒有說你必須檢查帖子型別是否存在:如果你新增了正確的操作,並且如果你呼叫 register_post_type,那麼定製的 post 型別必須存在:如果它不存在它是一個 WordPress 的問題。
記住:當你測試你的外掛,你必須測試你的程式碼,而不是 WordPress 程式碼。在你的測試中,你必須假設 WordPress(就像你使用的任何其他外部庫一樣) 執行良好。這就是單元測試的意義。
但在實踐中?
如果沒有載入 WordPress,如果您嘗試呼叫上面的類方法,您會發生致命錯誤,因此您需要模擬這些功能。
“manual” 方法
當然可以寫你的嘲笑 Library 或”manually” 模擬各種方法。這是可能的。我會告訴你如何做到這一點,但是我會給你一個簡單的方法。
如果 WordPress 在測試執行時未載入,則意味著您可以重新定義其功能,例如 add_action 或 register_post_type 。
我們假設你有一個檔案,從你的引導檔案載入,你有:
function add_action() {
global $counter;
if ( ! isset($counter['add_action']) ) {
$counter['add_action'] = array();
}
$counter['add_action'][] = func_get_args();
}
function register_post_type() {
global $counter;
if ( ! isset($counter['register_post_type']) ) {
$counter['register_post_type'] = array();
}
$counter['register_post_type'][] = func_get_args();
}
我將 re-wrote 的功能簡單地在每次呼叫時將元素新增到全域性陣列。
現在您應該建立 (如果您還沒有) 您自己的基礎測試用例類擴充套件了 PHPUnit_Framework_TestCase:它允許您輕鬆配置您的測試。
它可以是:
class Custom_TestCase extends PHPUnit_Framework_TestCase {
public function setUp() {
$GLOBALS['counter'] = array();
}
}
以這種方式,在每次測試之前,全域性計數器被複位。
現在你的測試程式碼 (我參考我上面釋出的重寫類):
class CustomPostTypes extends Custom_TestCase {
function test_init() {
global $counter;
$r = new RegisterCustomPostType;
$r->init();
$this->assertSame(
$counter['add_action'][0],
array( 'init', array( $r, 'register_post_type' ) )
);
}
function test_register_post_type() {
global $counter;
$r = new RegisterCustomPostType;
$r->register_post_type();
$this->assertSame( $counter['register_post_type'][0], array( 'foo' ) );
}
}
你應該注意:
-
我能夠單獨呼叫這兩種方法,而 WordPress 根本就沒有載入。這樣一來,如果一個測試失敗,我就知道究竟是誰的罪魁禍首。
-
正如我所說,這裡我測試類呼叫 WP 函式與期望的引數。沒有必要測試 CPT 是否真的存在。如果您正在測試 CPT 的存在,那麼您正在測試 WordPress 行為,而不是您的外掛行為…
尼斯.. 但它是一個 PITA!
是的,如果你必須手動模擬所有的 WordPress 功能,這真的是一個痛苦。我可以給出的一些一般建議是使用盡可能少的 WP 函式:您不必重寫 WordPress,而是您在自定義類中使用的抽象 WP 函式,以便它們可以被嘲笑和輕鬆測試。
例如。關於上面的例子,您可以編寫一個註冊帖子型別的類,在給定的引數的’init’ 上呼叫 register_post_type 。有了這個抽象,您仍然需要測試該類,但是在您註冊帖子型別的程式碼的其他地方,您可以使用該類,在測試中嘲笑它 (因此假設它有效) 。
令人敬畏的是,如果您編寫一個抽象 CPT 註冊的類,您可以為其建立一個單獨的儲存庫,並感謝 Composer 等現代工具將其嵌入到所有需要它的專案中:測試一次,使用無處不在。如果你發現了一個 bug,你可以在一個地方修復它,並用一個簡單的 composer update,所有使用它們的專案都是固定的。
第二次:編寫可隔離的程式碼是寫入更好的程式碼。
但遲早我需要在某個地方使用 WP 功能
當然。你永遠不應該與核心並行,這是沒有道理的。你可以編寫包含 WP 函式的類,但是這些類也需要測試。上述的”manual” 方法可以用於非常簡單的任務,但是當一個類包含大量的 WP 函式時,這可能是一個痛苦。
幸運的是,那裡有好的人寫好東西。最大的 WP 機構之一的 10up 為想要正確測試外掛的人們提供了一個非常好的 Library 。它是 WP_Mock 。
它允許你模擬 WP 功能的鉤子。假設你已經載入了你的測試 (見 repo 自述),我上面寫的同樣的測試成為:
class CustomPostTypes extends Custom_TestCase {
function test_init() {
$r = new RegisterCustomPostType;
// tests that the action was added with given arguments
WP_Mock::expectActionAdded( 'init', array( $r, 'register_post_type' ) );
$r->init();
}
function test_register_post_type() {
// tests that the function was called with given arguments and run once
WP_Mock::wpFunction( 'register_post_type', array(
'times' => 1,
'args' => array( 'foo' ),
) );
$r = new RegisterCustomPostType;
$r->register_post_type();
}
}
簡單,不是嗎?這個答案不是 WP_Mock 的教程,所以閱讀 repo readme 瞭解更多資訊,但上面的例子應該很清楚,我想。
此外,您不需要自己編寫任何嘲弄的 add_action 或 register_post_type,或保留任何全域性變數。
和 WP 類?
WP 也有一些類,如果執行測試時沒有載入 WordPress,則需要模擬它們。
這比嘲笑功能容易得多,PHPUnit 有一個嵌入式系統來模擬物件,但是在這裡我想向你推薦 Mockery 。這是一個非常強大的 Library ,非常易於使用。此外,它是 WP_Mock 的依賴,所以如果你有它,你也有 Mockery 。
但是 WP_UnitTestCase 怎麼樣?
WordPress 測試套件是為測試 WordPress 核心而建立的,如果您希望對核心做出貢獻,那麼它是至關重要的,但是使用它來進行外掛只會使您無法孤立地進行測試。
把你的眼睛放在 WP 世界:現在有很多 PHP 框架和 CMS,而且沒有一個建議使用框架程式碼來測試外掛/模組/擴充套件 (或任何它們被稱為) 。
如果你錯過工廠,這個套件的一個有用的功能,你必須知道那裡有 awesome things 。
奇蹟和缺點
有一種情況,我在這裡建議的工作流程缺少:自定義資料庫測試。
實際上,如果您使用標準的 WordPress 表和函式來寫入 (在最低階別的 $wpdb 方法),則您無需實際寫入資料或測試資料是否在資料庫中,只需確保使用適當的引數呼叫正確的方法。
但是,您可以使用自定義表和函式編寫外掛,從而構建查詢寫入,並測試這些查詢是否是您的責任。
在這些情況下,WordPress 測試套件可以幫助您很多,並且在某些情況下可能需要載入 WordPress 來執行像 dbDelta 這樣的功能。
(沒有必要說使用不同的 db 進行測試,不是嗎?)
幸運的是,PHPUnit 允許您組織您可以單獨執行的”suites” 中的測試,因此您可以編寫一個用於自定義資料庫測試的套件,您可以在其中載入 WordPress 環境 (或其中一部分),留下所有其餘測試 WordPress-free 。
只需要寫一些儘可能抽象儘可能多的資料庫操作的類,這樣所有其他外掛類都可以使用它們,這樣使用 mock 就可以正確地測試大多數類而不處理資料庫。
第三次,編寫程式碼可以輕鬆測試隔離意味著編寫更好的程式碼。
參考文獻
注:本文內容整合自 Google/Baidu/Bing 輔助翻譯的英文資料結果。如果您對結果不滿意,可以加入我們改善翻譯效果:薇曉朵技術論壇。
