แนวทางปฏิบัติที่ดีที่สุดในการทดสอบวิธีการป้องกันด้วย PHPUnit


287

ฉันพบการสนทนาในคุณทดสอบข้อมูลส่วนตัว

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

ฉันคิดถึงสิ่งต่อไปนี้:

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

แนวปฏิบัติที่ดีที่สุดคืออะไร มีอะไรอีกไหม?

ดูเหมือนว่า JUnit จะเปลี่ยนวิธีการป้องกันโดยอัตโนมัติเพื่อให้เป็นแบบสาธารณะ แต่ฉันไม่ได้มองลึกลงไป PHP ไม่ปล่อยให้เรื่องนี้ผ่านการสะท้อน


คำถามสองข้อ: 1. ทำไมคุณควรรบกวนฟังก์ชั่นการทดสอบชั้นเรียนของคุณไม่เปิดเผย? 2. ถ้าคุณควรทดสอบทำไมเป็นส่วนตัว
nad2000

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

4
และนี่คือรูปแบบการสนทนาและดังนั้นจึงไม่สร้างสรรค์ อีกครั้ง :)
mlvljr

72
คุณสามารถเรียกมันว่าผิดกฎของเว็บไซต์ แต่เพียงเรียกมันว่า "ไม่สร้างสรรค์" คือ ... มันเป็นการดูถูก
Andy V

1
@Visser มันเป็นการดูถูกตัวเอง;)
Pacerier

คำตอบ:


417

หากคุณใช้ PHP5 (> = 5.3.2) กับ PHPUnit คุณสามารถทดสอบวิธีการส่วนตัวและการป้องกันโดยใช้การไตร่ตรองเพื่อกำหนดให้เป็นสาธารณะก่อนที่จะทำการทดสอบ:

protected static function getMethod($name) {
  $class = new ReflectionClass('MyClass');
  $method = $class->getMethod($name);
  $method->setAccessible(true);
  return $method;
}

public function testFoo() {
  $foo = self::getMethod('foo');
  $obj = new MyClass();
  $foo->invokeArgs($obj, array(...));
  ...
}

27
ในการอ้างถึงลิงค์ไปยังบล็อกเซบาสเตียน: "ดังนั้น: เพียงเพราะการทดสอบคุณลักษณะและวิธีการป้องกันและส่วนตัวเป็นไปได้ไม่ได้หมายความว่านี่เป็น" สิ่งที่ดี " - เพื่อระลึกไว้เสมอ
edorian

10
ฉันจะประกวดสิ่งนั้น หากคุณไม่ต้องการวิธีการป้องกันหรือส่วนตัวในการทำงานอย่าทดสอบพวกเขา
uckelman

10
เพียงชี้แจงคุณไม่จำเป็นต้องใช้ PHPUnit เพื่อให้ทำงานได้ มันจะทำงานกับ SimpleTest หรืออะไรก็ได้ ไม่มีอะไรเกี่ยวกับคำตอบที่ขึ้นอยู่กับ PHPUnit
Ian Dunn

84
คุณไม่ควรทดสอบสมาชิกที่ได้รับการปกป้อง / ส่วนตัวโดยตรง พวกเขาเป็นของการดำเนินการภายในของชั้นเรียนและไม่ควรควบคู่ไปกับการทดสอบ สิ่งนี้ทำให้การปรับโครงสร้างเป็นไปไม่ได้และในที่สุดคุณจะไม่ทดสอบสิ่งที่ต้องทดสอบ คุณต้องทดสอบพวกเขาทางอ้อมโดยใช้วิธีการสาธารณะ หากคุณพบว่าสิ่งนี้ยากที่จะแน่ใจว่ามีปัญหากับองค์ประกอบของชั้นเรียนและคุณต้องแยกมันออกเป็นชั้นเล็ก ๆ โปรดจำไว้ว่าชั้นเรียนของคุณควรเป็นกล่องดำสำหรับการทดสอบของคุณ - คุณโยนอะไรซักอย่างแล้วเอากลับคืนและนั่นคือทั้งหมด!
gphilip

24
@ gphilip สำหรับฉันprotectedวิธีการก็เป็นส่วนหนึ่งของ api สาธารณะเพราะชั้นเรียนของบุคคลที่สามใด ๆ สามารถขยายและใช้งานได้โดยไม่ต้องใช้เวทมนตร์ ดังนั้นฉันคิดว่าprivateวิธีการเฉพาะที่อยู่ในหมวดหมู่ของวิธีการที่จะไม่ทดสอบโดยตรง protectedและpublicควรทดสอบโดยตรง
Filip Halaxa

48

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

class Foo {
  protected function stuff() {
    // secret stuff, you want to test
  }
}

class SubFoo extends Foo {
  public function exposedStuff() {
    return $this->stuff();
  }
}

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


2
คุณสามารถนำสิ่งต่าง ๆ () ไปใช้โดยตรงเป็นสาธารณะและคืนค่า parent :: stuff () ดูคำตอบของฉัน ดูเหมือนว่าฉันกำลังอ่านสิ่งเร็วเกินไปวันนี้
Michael Johnson

คุณถูก; สามารถเปลี่ยนวิธีการป้องกันเป็นวิธีสาธารณะได้
troelskn

ดังนั้นรหัสแนะนำตัวเลือกที่สามของฉันและ "โปรดทราบว่าคุณสามารถแทนที่การสืบทอดด้วยการแต่งเพลง" ไปในทิศทางของตัวเลือกแรกของฉันหรือrefactoring.com/catalog/replaceInheritanceWithDelegation.html
GrGr

34
ฉันไม่เห็นด้วยว่ามันเป็นสัญญาณที่ไม่ดี มาสร้างความแตกต่างระหว่าง TDD กับการทดสอบหน่วย การทดสอบหน่วยควรทดสอบวิธีการส่วนตัว IMO เนื่องจากสิ่งเหล่านี้เป็นหน่วยและจะได้รับประโยชน์เช่นเดียวกับวิธีการทดสอบหน่วยสาธารณะวิธีการที่ได้รับประโยชน์จากการทดสอบหน่วย
koen

36
วิธีการป้องกันเป็นส่วนหนึ่งของอินเทอร์เฟซของคลาสพวกเขาไม่เพียง แต่รายละเอียดการใช้งาน จุดรวมของสมาชิกที่ได้รับการป้องกันคือเพื่อให้ subclassers (ผู้ใช้ในสิทธิของตนเอง) สามารถใช้วิธีการป้องกันเหล่านั้นภายใน exstions ชั้นเรียน ต้องมีการทดสอบอย่างชัดเจน
BT

40

teastburnมีแนวทางที่ถูกต้อง วิธีที่ง่ายกว่าคือโทรหาวิธีนี้โดยตรงและคืนคำตอบ:

class PHPUnitUtil
{
  public static function callMethod($obj, $name, array $args) {
        $class = new \ReflectionClass($obj);
        $method = $class->getMethod($name);
        $method->setAccessible(true);
        return $method->invokeArgs($obj, $args);
    }
}

คุณสามารถเรียกสิ่งนี้ได้ง่ายๆในแบบทดสอบของคุณโดย:

$returnVal = PHPUnitUtil::callMethod(
                $this->object,
                '_nameOfProtectedMethod', 
                array($arg1, $arg2)
             );

1
นี่เป็นตัวอย่างที่ดีมากขอบคุณ วิธีนี้ควรเป็นแบบสาธารณะแทนที่จะได้รับการปกป้องใช่หรือไม่
valk

จุดดี. ฉันใช้วิธีนี้จริงในคลาสพื้นฐานของฉันที่ฉันขยายคลาสการทดสอบของฉันจากซึ่งในกรณีนี้เหมาะสม แม้ว่าชื่อของชั้นจะผิดที่นี่
robert.egginton

ฉันทำรหัสชิ้นเดียวกันตาม teastburn xD
Nebulosar

23

ฉันต้องการที่จะนำเสนอการเปลี่ยนแปลงเล็กน้อยเพื่อ GetMethod () ที่กำหนดไว้ในคำตอบของ uckelman

รุ่นนี้มีการเปลี่ยนแปลง getMethod () โดยการลบค่าฮาร์ดโค้ดและทำให้การใช้งานง่ายขึ้นเล็กน้อย ฉันขอแนะนำให้เพิ่มลงในคลาส PHPUnitUtil ของคุณดังในตัวอย่างด้านล่างหรือในคลาส PHPUnit_Framework_TestCase ที่ขยายเพิ่มของคุณ

เนื่องจาก MyClass กำลังอินสแตนซ์ anyways และ ReflectionClass สามารถใช้สตริงหรือวัตถุ ...

class PHPUnitUtil {
    /**
     * Get a private or protected method for testing/documentation purposes.
     * How to use for MyClass->foo():
     *      $cls = new MyClass();
     *      $foo = PHPUnitUtil::getPrivateMethod($cls, 'foo');
     *      $foo->invoke($cls, $...);
     * @param object $obj The instantiated instance of your class
     * @param string $name The name of your private/protected method
     * @return ReflectionMethod The method you asked for
     */
    public static function getPrivateMethod($obj, $name) {
      $class = new ReflectionClass($obj);
      $method = $class->getMethod($name);
      $method->setAccessible(true);
      return $method;
    }
    // ... some other functions
}

ฉันยังสร้างฟังก์ชันนามแฝง getProtectedMethod () เพื่อให้ชัดเจนถึงสิ่งที่คาดหวัง แต่สิ่งนั้นขึ้นอยู่กับคุณ

ไชโย!


+1 สำหรับการใช้ Reflection Class API
Bill Ortell

10

ฉันคิดว่า troelskn อยู่ใกล้ ฉันจะทำสิ่งนี้แทน:

class ClassToTest
{
   protected function testThisMethod()
   {
     // Implement stuff here
   }
}

จากนั้นให้ใช้สิ่งนี้:

class TestClassToTest extends ClassToTest
{
  public function testThisMethod()
  {
    return parent::testThisMethod();
  }
}

จากนั้นคุณรันการทดสอบกับ TestClassToTest

มันควรจะเป็นไปได้ที่จะสร้างคลาสส่วนขยายดังกล่าวโดยอัตโนมัติโดยการแยกรหัส ฉันจะไม่แปลกใจถ้า PHPUnit มีกลไกดังกล่าวอยู่แล้ว (แม้ว่าฉันยังไม่ได้ตรวจสอบ)


หึ ... มันดูเหมือนว่าฉันพูดใช้ตัวเลือกที่สามของคุณ :)
ไมเคิลจอห์นสัน

2
ใช่นั่นคือตัวเลือกที่สามของฉัน ฉันค่อนข้างมั่นใจว่า PHPUnit ไม่มีกลไกดังกล่าว
GrGr

สิ่งนี้จะไม่ทำงานคุณไม่สามารถแทนที่ฟังก์ชันที่มีการป้องกันด้วยฟังก์ชันสาธารณะที่มีชื่อเดียวกันได้
โคเอ็น

ฉันอาจจะผิด แต่ฉันไม่คิดว่าวิธีนี้สามารถใช้งานได้ PHPUnit (เท่าที่ฉันเคยใช้) ต้องการให้คลาสการทดสอบของคุณขยายคลาสอื่นที่มีฟังก์ชันการทดสอบจริง หากไม่มีวิธีการที่ฉันไม่แน่ใจว่าฉันจะเห็นว่าสามารถใช้คำตอบนี้ได้อย่างไร phpunit.de/manual/current/en/…
Cypher

1
FYI onl นี้ใช้ได้กับวิธีที่ได้รับการป้องกันไม่ใช่สำหรับไพรเวต
Sliq

5

ฉันจะโยนหมวกของฉันเข้าไปในวงแหวนที่นี่:

ฉันใช้แฮ็ค __ Call ด้วยระดับความสำเร็จที่หลากหลาย ทางเลือกที่ฉันใช้คือใช้รูปแบบของผู้เข้าชม:

1: สร้างคลาส stdClass หรือแบบกำหนดเอง (เพื่อบังคับใช้ชนิด)

2: ไพรม์ที่มีเมธอดและอาร์กิวเมนต์ที่ต้องการ

3: ตรวจสอบให้แน่ใจว่า SUT ของคุณมีเมธอด acceptVisitor ซึ่งจะดำเนินการเมธอดด้วยอาร์กิวเมนต์ที่ระบุในคลาสการเยี่ยมชม

4: ฉีดเข้าไปในชั้นเรียนที่คุณต้องการทดสอบ

5: SUT จะฉีดผลลัพธ์ของการดำเนินการไปยังผู้เข้าชม

6: ใช้เงื่อนไขการทดสอบของคุณกับแอตทริบิวต์ผลลัพธ์ของผู้เข้าชม


1
+1 สำหรับโซลูชันที่น่าสนใจ
jsh

5

คุณสามารถใช้ __call () ในแบบทั่วไปเพื่อเข้าถึงวิธีการป้องกัน เพื่อให้สามารถทดสอบชั้นนี้

class Example {
    protected function getMessage() {
        return 'hello';
    }
}

คุณสร้างคลาสย่อยใน ExampleTest.php:

class ExampleExposed extends Example {
    public function __call($method, array $args = array()) {
        if (!method_exists($this, $method))
            throw new BadMethodCallException("method '$method' does not exist");
        return call_user_func_array(array($this, $method), $args);
    }
}

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

ตอนนี้กรณีทดสอบเองจะแตกต่างกันในตำแหน่งที่คุณสร้างวัตถุที่จะทดสอบเท่านั้นการสลับใน ExampleExposed สำหรับตัวอย่าง

class ExampleTest extends PHPUnit_Framework_TestCase {
    function testGetMessage() {
        $fixture = new ExampleExposed();
        self::assertEquals('hello', $fixture->getMessage());
    }
}

ฉันเชื่อว่า PHP 5.3 ช่วยให้คุณใช้การสะท้อนเพื่อเปลี่ยนการเข้าถึงวิธีการโดยตรง แต่ฉันคิดว่าคุณต้องทำเช่นนั้นสำหรับแต่ละวิธี


1
การใช้งาน __call () นั้นยอดเยี่ยม! ฉันพยายามลงคะแนน แต่ฉันไม่ได้ลงคะแนนจนกว่าฉันจะทดสอบวิธีนี้และตอนนี้ฉันไม่ได้รับอนุญาตให้ลงคะแนนเนื่องจากมีการ จำกัด เวลาใน SO
Adam Franco

call_user_method_array()ฟังก์ชั่นจะเลิกเป็นของ PHP 4.1.0 ... ใช้call_user_func_array(array($this, $method), $args)แทน โปรดทราบว่าถ้าคุณใช้ PHP 5.3.2+ คุณสามารถใช้ Reflection เพื่อเข้าถึงวิธีการและคุณสมบัติแบบป้องกัน / ส่วนตัว
nuqqsa

@nuqqsa - ขอบคุณฉันได้อัปเดตคำตอบแล้ว ฉันได้เขียนตั้งแต่Accessibleแพคเกจทั่วไปที่ใช้การสะท้อนเพื่อให้การทดสอบเพื่อเข้าถึงคุณสมบัติส่วนตัว / ป้องกันและวิธีการเรียนและวัตถุ
David Harkness

รหัสนี้ใช้ไม่ได้กับฉันใน PHP 5.2.7 - เมธอด __call ไม่ได้ถูกเรียกใช้สำหรับเมธอดที่คลาสพื้นฐานกำหนด ฉันไม่พบเอกสาร แต่ฉันคิดว่าพฤติกรรมนี้เปลี่ยนไปใน PHP 5.3 (ที่ฉันยืนยันว่าใช้ได้)
Russell Davis

@Russell - __call()รับการเรียกใช้เฉพาะเมื่อผู้เรียกไม่สามารถเข้าถึงวิธีการ ตั้งแต่ชั้นเรียนและ subclasses __call()มีการเข้าถึงวิธีการป้องกันการโทรไปยังพวกเขาจะไม่ผ่าน คุณช่วยโพสต์โค้ดที่ใช้ไม่ได้ใน 5.2.7 ในคำถามใหม่หรือไม่? ฉันใช้ข้างต้นใน 5.2 และย้ายไปใช้การสะท้อนกับ 5.3.2 เท่านั้น
David Harkness

2

ฉันขอแนะนำวิธีแก้ไขปัญหาต่อไปนี้สำหรับวิธีแก้ปัญหา / แนวคิดของ "Henrik Paul" :)

คุณรู้ชื่อของวิธีการส่วนตัวในชั้นเรียนของคุณ ตัวอย่างเช่นพวกเขาเป็น _add (), _edit (), _delete () ฯลฯ

ดังนั้นเมื่อคุณต้องการทดสอบจากแง่มุมของการทดสอบหน่วยเพียงแค่เรียกเมธอดส่วนตัวโดยการเติมคำนำหน้าและ / หรือต่อท้ายทั่วไปคำคำ (เช่น _addPhpunit) เพื่อที่ว่าเมื่อ __call () วิธีการถูกเรียก (ตั้งแต่ method _addPhpunit () ไม่ มีอยู่) ของคลาสเจ้าของคุณเพียงแค่ใส่โค้ดที่จำเป็นในเมธอด __call () เพื่อลบคำ / ส่วนต่อท้ายที่นำหน้า / s (Phpunit) จากนั้นจึงเรียกวิธีส่วนตัวที่อนุมานจากที่นั่น นี่เป็นอีกวิธีที่ดีในการใช้เวทมนตร์

ลองดู

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.