問題描述

我正在使用 TDD 開發一個外掛,而我完全無法測試的一件事是鉤子。

我的意思是 OK,我可以測試鉤子回撥,但是如何測試鉤子是否實際觸發 (兩個自定義鉤子和 WordPress 預設鉤子)?我假設有些嘲弄會有所幫助,但是我根本找不到我失蹤的東西。

我安裝了測試套件與 WP-CLI 。根據 this answerinit 鉤應該觸發,但… 它不會; 程式碼也在 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_actionregister_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_actionregister_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 輔助翻譯的英文資料結果。如果您對結果不滿意,可以加入我們改善翻譯效果:薇曉朵技術論壇。