การทดสอบ hooks callback


34

ฉันกำลังพัฒนาปลั๊กอินโดยใช้ TDD และสิ่งหนึ่งที่ฉันล้มเหลวในการทดสอบอย่างสมบูรณ์คือ ... hooks

ฉันหมายถึงตกลงฉันสามารถทดสอบการติดต่อกลับของ hook แต่ฉันจะทดสอบได้อย่างไรว่าเบ็ดเรียกจริงหรือไม่ (ทั้ง hooks ที่กำหนดเองและตะขอเริ่มต้นของ WordPress)? ฉันคิดว่าการเยาะเย้ยบางอย่างจะช่วยได้ แต่ฉันก็ไม่สามารถเข้าใจได้ว่าฉันพลาดอะไรไป

ฉันติดตั้งชุดทดสอบด้วย WP-CLI ตามคำตอบนี้ , initเบ็ดควรเรียก แต่ ... มันไม่ได้; นอกจากนี้รหัสทำงานภายใน WordPress

จากความเข้าใจของฉัน bootstrap จะถูกโหลดครั้งสุดท้ายดังนั้นจึงเป็นเรื่องที่สมเหตุสมผลที่จะไม่เรียกใช้ init ดังนั้นคำถามที่เหลืออยู่คือ: heck i ควรทดสอบว่า hooks ถูกเรียกใช้อย่างไร

ขอบคุณ!

ไฟล์ bootstrap มีลักษณะดังนี้:

$_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' ) );
  }
}

ขอบคุณ!


หากคุณใช้งานอยู่phpunitคุณจะเห็นการทดสอบที่ล้มเหลวหรือผ่านการทดสอบแล้วหรือไม่ คุณไม่ติดตั้งbin/install-wp-tests.sh?
Sven

ฉันคิดว่าส่วนหนึ่งของปัญหาคืออาจRegisterCustomPostType::__construct()ไม่เคยถูกเรียกเมื่อมีการโหลดปลั๊กอินสำหรับการทดสอบ นอกจากนี้ยังเป็นไปได้ที่คุณจะได้รับผลกระทบจากข้อผิดพลาด # 29827 ; อาจลองอัปเดตชุดทดสอบหน่วยของ WP เวอร์ชันของคุณ
JD

@Sven: ใช่การทดสอบล้มเหลว ฉันติดตั้งbin/install-wp-tests.sh(ตั้งแต่ฉันใช้ wp-cli) @JD: RegisterCustomPostType :: __ โครงสร้างถูกเรียก (เพิ่งเพิ่มdie()คำสั่งและ phpunit หยุดอยู่ตรงนั้น)
Ionut Staicu

ฉันไม่แน่ใจในด้านการทดสอบหน่วย (ไม่ใช่มือขวาของฉัน) แต่จากมุมมองตามตัวอักษรคุณสามารถใช้did_action()เพื่อตรวจสอบว่ามีการกระทำที่ถูกไล่ออกหรือไม่
Rarst

@Rarst: ขอบคุณสำหรับคำแนะนำ แต่ก็ยังไม่ทำงาน ด้วยเหตุผลบางอย่างฉันคิดว่าเวลาไม่ถูกต้อง (ทดสอบวิ่งก่อนinitขอ)
Ionut Staicu

คำตอบ:


72

ทดสอบแยก

เมื่อพัฒนาปลั๊กอินวิธีที่ดีที่สุดในการทดสอบคือไม่ต้องโหลดสภาพแวดล้อม WordPress

ถ้าคุณเขียนโค้ดที่สามารถทดสอบได้อย่างง่ายดายโดยไม่ WordPress, รหัสของคุณจะกลายเป็นดีกว่า

ทุกองค์ประกอบที่ถูกทดสอบหน่วยควรได้รับการทดสอบแยก : เมื่อคุณทดสอบคลาสคุณจะต้องทดสอบเฉพาะคลาสนั้นโดยสมมติว่าโค้ดอื่น ๆ นั้นทำงานได้อย่างสมบูรณ์

ตัวแยก

นี่คือเหตุผลที่การทดสอบหน่วยเรียกว่า "หน่วย"

ในฐานะที่เป็นประโยชน์เพิ่มเติมหากไม่มีการโหลดคอร์การทดสอบของคุณจะทำงานได้เร็วขึ้นมาก

หลีกเลี่ยง hooks ในตัวสร้าง

เคล็ดลับที่ฉันสามารถให้คุณได้คือหลีกเลี่ยงการใส่ตะขอในตัวสร้าง นั่นเป็นหนึ่งในสิ่งที่จะทำให้รหัสของคุณทดสอบได้โดยแยกออกจากกัน

ลองดูรหัสทดสอบใน 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วิธีที่ผมเขียนชั้นนี้ช่วยให้คุณสร้างอินสแตนซ์ของชั้นโดยไม่ต้องโทร

ในชั้นเรียนด้านบนมี 2 สิ่งที่ต้องทำการทดสอบ:

  • วิธีการโทรinit จริงadd_actionผ่านไปยังอาร์กิวเมนต์ที่เหมาะสม
  • วิธีการregister_post_type จริงเรียกregister_post_typeฟังก์ชั่น

ฉันไม่ได้บอกว่าคุณต้องตรวจสอบว่ามีประเภทโพสต์หรือไม่: ถ้าคุณเพิ่มการกระทำที่เหมาะสมและถ้าคุณเรียกregister_post_typeใช้ประเภทโพสต์ที่กำหนดเองจะต้องมีอยู่: หากไม่มีอยู่นั้นเป็นปัญหาของ WordPress

โปรดจำไว้ว่าเมื่อคุณทดสอบปลั๊กอินของคุณคุณมีการทดสอบของคุณรหัสไม่รหัส WordPress ในการทดสอบของคุณคุณต้องสมมติว่า WordPress (เช่นเดียวกับห้องสมุดภายนอกอื่น ๆ ที่คุณใช้) นั้นทำงานได้ดี นั่นคือความหมายของการทดสอบหน่วย

แต่ ... ในทางปฏิบัติ

หาก WordPress ไม่โหลดถ้าคุณพยายามเรียกวิธีการเรียนด้านบนคุณจะได้รับข้อผิดพลาดร้ายแรงดังนั้นคุณต้องจำลองฟังก์ชั่น

วิธีการ "ด้วยตนเอง"

ตรวจสอบให้แน่ใจว่าคุณสามารถเขียนไลบรารีที่เยาะเย้ยหรือจำลอง "ด้วยตนเอง" ได้ทุกวิธี มันเป็นไปได้. ฉันจะบอกวิธีการทำ แต่แล้วฉันจะแสดงวิธีที่ง่ายขึ้น

หาก WordPress ไม่โหลดในขณะที่การทดสอบกำลังทำงานอยู่ก็หมายความว่าคุณสามารถ redefine หน้าที่ของตนเช่นหรือadd_actionregister_post_type

สมมติว่าคุณมีไฟล์โหลดจากไฟล์ bootstrap ที่คุณมี:

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();
}

ฉันเขียนฟังก์ชันใหม่เพื่อเพิ่มองค์ประกอบให้กับอาร์เรย์ระดับโลกทุกครั้งที่มีการเรียกใช้

ตอนนี้คุณควรสร้าง (ถ้าคุณยังไม่มี) การขยายคลาสเคสทดสอบพื้นฐานของคุณ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 แบบนามธรรมที่คุณใช้ในคลาสที่กำหนดเองเพื่อให้สามารถจำลองและทดสอบได้อย่างง่ายดาย

เช่นเกี่ยวกับตัวอย่างข้างต้นคุณสามารถเขียนคลาสที่ลงทะเบียนประเภทการโพสต์เรียกregister_post_typeใช้ 'init' พร้อมอาร์กิวเมนต์ที่กำหนด ด้วยนามธรรมนี้คุณยังต้องทดสอบคลาสนั้น แต่ในที่อื่น ๆ ของรหัสของคุณที่ลงทะเบียนประเภทโพสต์คุณสามารถใช้คลาสนั้นเยาะเย้ยในการทดสอบ (ดังนั้นสมมติว่ามันใช้งานได้)

สิ่งที่น่ากลัวคือถ้าคุณเขียนชั้นเรียนว่าการลงทะเบียน CPT บทคัดย่อคุณสามารถสร้างพื้นที่เก็บข้อมูลที่แยกต่างหากสำหรับมันและขอบคุณที่เครื่องมือที่ทันสมัยเช่นนักแต่งเพลงที่ฝังไว้ในทุกโครงการที่คุณต้องการ: การทดสอบครั้งเดียวใช้งานได้ทุกที่ และหากคุณเคยพบข้อผิดพลาดในนั้นคุณสามารถแก้ไขได้ในที่เดียวและด้วยcomposer updateโครงการทั้งหมดที่ใช้งานง่ายได้รับการแก้ไขด้วย

สำหรับครั้งที่สอง: การเขียนโค้ดที่สามารถทดสอบแยกได้หมายถึงการเขียนโค้ดที่ดีขึ้น

แต่ไม่ช้าก็เร็วฉันจำเป็นต้องใช้ฟังก์ชั่น WP บางแห่ง ...

แน่นอน. คุณไม่ควรทำตัวขนานกับแกนมันไม่สมเหตุสมผล คุณสามารถเขียนคลาสที่ล้อมฟังก์ชัน WP แต่คลาสเหล่านั้นจำเป็นต้องทดสอบด้วย วิธีการ "ด้วยตนเอง" ที่อธิบายไว้ข้างต้นอาจถูกใช้สำหรับงานง่าย ๆ แต่เมื่อคลาสมีฟังก์ชัน WP จำนวนมากอาจเป็นความเจ็บปวด

โชคดีที่นั่นมีคนดีที่เขียนสิ่งที่ดี 10upซึ่งเป็นหนึ่งในเอเจนซี่ WP ที่ใหญ่ที่สุดยังคงรักษาไลบรารี่ที่ยอดเยี่ยมสำหรับผู้ที่ต้องการทดสอบปลั๊กอินอย่างถูกวิธี WP_Mockมันเป็น

จะช่วยให้คุณฟังก์ชั่น WP จำลองตะขอ สมมติว่าคุณโหลดในการทดสอบของคุณ (ดู repo readme) การทดสอบเดียวกันที่ฉันเขียนข้างต้นกลายเป็น:

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 มีระบบฝังตัวเพื่อจำลองวัตถุ แต่ที่นี่ฉันต้องการแนะนำการเยาะเย้ยให้กับคุณ มันเป็นห้องสมุดที่ทรงพลังมากและใช้งานง่ายมาก ยิ่งกว่านั้นมันเป็นสิ่งที่ต้องพึ่งพาWP_Mockดังนั้นถ้าคุณมีคุณก็จะมีการเยาะเย้ยเช่นกัน

แต่เกี่ยวกับWP_UnitTestCaseอะไร

ชุดทดสอบ WordPress ถูกสร้างขึ้นเพื่อทดสอบหลักของ WordPress และหากคุณต้องการมีส่วนร่วมในแกนหลักนั้นเป็นสิ่งสำคัญ แต่การใช้มันสำหรับปลั๊กอินจะทำให้คุณไม่ได้แยก

ทำให้สายตาของคุณเหนือโลก WP: มีเฟรมเวิร์คสเตชั่นของ PHP และ CMS อยู่มากมายและไม่มีใครแนะนำให้ทดสอบปลั๊กอิน / โมดูล / ส่วนขยาย (หรือสิ่งที่พวกเขาเรียกว่า) โดยใช้โค้ดเฟรมเวิร์ก

หากคุณพลาดโรงงานคุณลักษณะที่มีประโยชน์ของชุดคุณต้องรู้ว่ามีสิ่งที่น่ากลัวอยู่ที่นั่น

Gotchas และข้อเสีย

มีกรณีคือเมื่อขั้นตอนการทำงานที่ผมแนะนำที่นี่ขาด: การทดสอบฐานข้อมูลที่กำหนดเอง

ในความเป็นจริงถ้าคุณใช้ตาราง WordPress มาตรฐานและฟังก์ชั่นในการเขียนมี (ที่ระดับต่ำสุด$wpdbวิธีการ) คุณไม่จำเป็นต้องจริงเขียนข้อมูลหรือการทดสอบหากข้อมูลเป็นจริงในฐานข้อมูลเพียงให้แน่ใจว่าวิธีการที่เหมาะสมจะเรียกว่ามีข้อโต้แย้งที่เหมาะสม

อย่างไรก็ตามคุณสามารถเขียนปลั๊กอินด้วยตารางและฟังก์ชั่นที่กำหนดเองที่สร้างคิวรีเพื่อเขียนและทดสอบว่าคิวรี่เหล่านั้นทำงานได้หรือไม่

ในกรณีผู้ WordPress ชุดทดสอบที่สามารถช่วยให้คุณมากและโหลด WordPress dbDeltaอาจมีความจำเป็นในบางกรณีที่จะเรียกใช้ฟังก์ชั่นเช่น

(ไม่จำเป็นต้องบอกว่าใช้ db อื่นสำหรับการทดสอบใช่ไหม?)

โชคดีที่ PHPUnit ช่วยให้คุณสามารถจัดระเบียบการทดสอบของคุณได้ใน "สวีท" ที่สามารถทำงานแยกกันเพื่อให้คุณสามารถเขียนชุดสำหรับการทดสอบฐานข้อมูลที่กำหนดเองที่คุณโหลดสภาพแวดล้อม WordPress (หรือส่วนหนึ่งของมัน) ออกจากส่วนที่เหลือทั้งหมดของการทดสอบของคุณWordPress ฟรี

ให้แน่ใจว่าได้เขียนคลาสที่เป็นนามธรรมการดำเนินการฐานข้อมูลมากที่สุดเท่าที่เป็นไปได้ในลักษณะที่คลาสปลั๊กอินอื่น ๆ ใช้ประโยชน์จากคลาสนั้นเพื่อให้การใช้ mocks คุณสามารถทดสอบคลาสส่วนใหญ่โดยไม่ต้องจัดการกับฐานข้อมูล

สำหรับครั้งที่สามการเขียนโค้ดที่สามารถทดสอบแยกได้ง่ายหมายถึงการเขียนโค้ดที่ดีกว่า


5
อึศักดิ์สิทธิ์ข้อมูลที่มีประโยชน์มากมาย! ขอขอบคุณ! ยังไงก็เถอะฉันก็พลาดการทดสอบหน่วยทั้งหมด (จนถึงตอนนี้ฉันกำลังทดสอบ PHP อยู่เฉพาะใน Code Dojo) ฉันยังพบเกี่ยวกับ wp_mock ก่อนหน้านี้ในวันนี้ แต่ด้วยเหตุผลบางอย่างที่ฉันจัดการไม่สนใจ สิ่งที่ทำให้ฉันโกรธคือการทดสอบใด ๆ ไม่ว่ามันจะเล็กแค่ไหนก็ใช้เวลาอย่างน้อยสองวินาทีในการทำงาน (โหลด WP env ก่อนดำเนินการทดสอบที่สอง) ขอบคุณอีกครั้งที่เปิดตาของฉัน!
Ionut Staicu

4
ขอบคุณ @IonutStaicu ฉันลืมที่จะกล่าวว่าไม่โหลด WordPress ทำให้การทดสอบของคุณเร็วขึ้นมาก
gmazzap

6
นอกจากนี้ยังมีค่าที่ชี้ให้เห็นว่ากรอบการทดสอบหน่วย WP Core เป็นเครื่องมือที่น่าทึ่งสำหรับการทดสอบแบบบูรณาการซึ่งจะเป็นการทดสอบแบบอัตโนมัติเพื่อให้แน่ใจว่ามันทำงานได้ดีกับตัว WP เอง (เช่นไม่มีการชนกันของชื่อฟังก์ชันโดยบังเอิญ ฯลฯ )
John P Bloch

1
@ JohnPBloch +1 เพื่อจุดดี แม้ว่าการใช้เนมสเปซก็เพียงพอที่จะหลีกเลี่ยงการชนกันของชื่อฟังก์ชั่นใน WordPress ที่ซึ่งทุกอย่างเป็นสากล :) แต่แน่นอนว่าการบูรณาการ / การทดสอบการใช้งานเป็นสิ่งสำคัญ ฉันกำลังเล่นกับ Behat + Mink ในขณะนี้ แต่ฉันก็ยังฝึกอยู่
gmazzap

1
ขอบคุณสำหรับ "การนั่งเฮลิคอปเตอร์" เหนือป่า UnitTest ของ WordPress - ฉันยังคงหัวเราะเยาะภาพมหากาพย์นั้น ;-)
birgire
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.