Praktik terbaik untuk menguji metode yang dilindungi dengan PHPUnit

287

Saya menemukan diskusi tentang Apakah Anda menguji metode pribadi informatif.

Saya telah memutuskan, bahwa di beberapa kelas, saya ingin memiliki metode yang dilindungi, tetapi mengujinya. Beberapa metode ini statis dan pendek. Karena sebagian besar metode publik memanfaatkannya, saya mungkin akan dapat dengan aman menghapus tes nanti. Tetapi untuk memulai dengan pendekatan TDD dan menghindari debugging, saya benar-benar ingin mengujinya.

Saya memikirkan hal berikut:

  • Metode Obyek seperti yang disarankan dalam jawaban tampaknya terlalu banyak untuk ini.
  • Mulailah dengan metode publik dan ketika cakupan kode diberikan oleh tes tingkat yang lebih tinggi, jadikan kode tersebut terlindungi dan hapus tes.
  • Mewarisi kelas dengan antarmuka yang dapat diuji membuat metode yang dilindungi publik

Yang merupakan praktik terbaik? Apakah ada hal lain?

Tampaknya, JUnit secara otomatis mengubah metode yang dilindungi menjadi publik, tapi saya tidak melihatnya lebih dalam. PHP tidak mengizinkan ini melalui refleksi .

GrGr
sumber
Dua pertanyaan: 1. mengapa Anda harus repot-repot menguji fungsionalitas yang tidak diungkapkan oleh kelas Anda? 2. Jika Anda harus mengujinya, mengapa itu pribadi?
nad2000
2
Mungkin dia ingin menguji apakah properti pribadi diatur dengan benar dan satu-satunya cara pengujian hanya menggunakan fungsi setter adalah membuat properti pribadi menjadi publik dan memeriksa data
AntonioCS
4
Jadi ini adalah gaya diskusi dan karenanya tidak konstruktif. Lagi :)
mlvljr
72
Anda dapat menyebutnya melanggar aturan situs, tetapi menyebutnya "tidak konstruktif" adalah ... itu menghina.
Andy V
1
@ Visser, Ini menghina dirinya sendiri;)
Pacerier

Jawaban:

417

Jika Anda menggunakan PHP5 (> = 5.3.2) dengan PHPUnit, Anda dapat menguji metode pribadi dan terlindungi dengan menggunakan refleksi untuk mengaturnya agar publik sebelum menjalankan tes Anda:

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(...));
  ...
}
uckelman
sumber
27
Mengutip tautan ke blog sebastians: "Jadi: Hanya karena pengujian atas atribut dan metode yang dilindungi dan pribadi tidak mungkin tidak berarti bahwa ini adalah" hal yang baik "." - Hanya untuk mengingatnya
edorian
10
Saya akan menentang itu. Jika Anda tidak membutuhkan metode yang dilindungi atau pribadi untuk bekerja, jangan mengujinya.
uckelman
10
Hanya untuk memperjelas, Anda tidak perlu menggunakan PHPUnit agar ini berfungsi. Ini juga akan bekerja dengan SimpleTest atau apa pun. Tidak ada jawaban yang tergantung pada PHPUnit.
Ian Dunn
84
Anda tidak boleh menguji anggota yang dilindungi / pribadi secara langsung. Mereka termasuk dalam implementasi internal kelas, dan tidak boleh digabungkan dengan tes. Ini membuat refactoring tidak mungkin dan pada akhirnya Anda tidak menguji apa yang perlu diuji. Anda perlu mengujinya secara tidak langsung menggunakan metode publik. Jika Anda menemukan ini sulit, hampir yakin bahwa ada masalah dengan komposisi kelas dan Anda perlu memisahkannya ke kelas yang lebih kecil. Ingatlah bahwa kelas Anda harus menjadi kotak hitam untuk ujian Anda - Anda melempar sesuatu dan Anda mendapatkan sesuatu kembali, dan itu saja!
gphilip
24
@ gphilip Bagi saya, protectedmetode juga merupakan bagian dari api publik karena setiap kelas pihak ketiga dapat memperluas dan menggunakannya tanpa sihir. Jadi saya pikir hanya privatemetode yang masuk dalam kategori metode yang tidak akan diuji secara langsung. protecteddan publicharus langsung diuji.
Filip Halaxa
48

Kamu sepertinya sudah sadar, tapi aku akan menyatakannya kembali; Ini pertanda buruk, jika Anda perlu menguji metode yang dilindungi. Tujuan dari tes unit, adalah untuk menguji antarmuka kelas, dan metode yang dilindungi adalah detail implementasi. Yang mengatakan, ada kasus di mana itu masuk akal. Jika Anda menggunakan warisan, Anda dapat melihat superclass sebagai menyediakan antarmuka untuk subkelas. Jadi di sini, Anda harus menguji metode yang dilindungi (Tapi tidak pernah metode pribadi ). Solusi untuk ini, adalah membuat subclass untuk tujuan pengujian, dan menggunakannya untuk mengekspos metode. Misalnya.:

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

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

Perhatikan bahwa Anda selalu dapat mengganti warisan dengan komposisi. Saat menguji kode, biasanya jauh lebih mudah untuk berurusan dengan kode yang menggunakan pola ini, jadi Anda mungkin ingin mempertimbangkan opsi itu.

troelskn
sumber
2
Anda bisa langsung mengimplementasikan stuff () sebagai publik dan mengembalikan parent :: stuff (). Lihat tanggapan saya. Sepertinya saya membaca hal-hal terlalu cepat hari ini.
Michael Johnson
Kamu benar; Ini valid untuk mengubah metode yang dilindungi menjadi metode publik.
troelskn
Jadi kode menyarankan opsi ketiga saya dan "Perhatikan bahwa Anda selalu dapat mengganti warisan dengan komposisi." berjalan ke arah opsi pertama saya atau refactoring.com/catalog/replaceInheritanceWithDelegation.html
GrGr
34
Saya tidak setuju bahwa itu pertanda buruk. Mari kita membuat perbedaan antara TDD dan Unit Testing. Pengujian unit harus menguji metode pribadi juga, karena ini adalah unit dan akan mendapat manfaat dengan cara yang sama seperti pengujian unit metode publik mendapat manfaat dari pengujian unit.
koen
36
Metode yang dilindungi adalah bagian dari antarmuka kelas, mereka bukan hanya detail implementasi. Inti dari anggota yang dilindungi adalah agar subclasser (pengguna yang memiliki hak sendiri) dapat menggunakan metode yang dilindungi itu di dalam pengusiran kelas. Itu jelas perlu diuji.
BT
40

teastburn memiliki pendekatan yang tepat. Lebih sederhana lagi adalah dengan memanggil metode secara langsung dan mengembalikan jawabannya:

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

Anda dapat memanggil ini hanya dalam tes Anda dengan:

$returnVal = PHPUnitUtil::callMethod(
                $this->object,
                '_nameOfProtectedMethod', 
                array($arg1, $arg2)
             );
robert.egginton
sumber
1
Ini adalah contoh yang bagus, terima kasih. Metode ini harus bersifat publik, bukan dilindungi, bukan?
valk
Poin yang bagus. Saya benar-benar menggunakan metode ini di kelas dasar saya untuk memperluas kelas tes saya, dalam hal ini masuk akal. Namun nama kelasnya salah di sini.
robert.egginton
Saya membuat kode yang sama persis berdasarkan pada teastburn xD
Nebulosar
23

Saya ingin mengusulkan sedikit variasi untuk getMethod () didefinisikan dalam jawaban uckelman .

Versi ini mengubah getMethod () dengan menghapus nilai-nilai hard-coded dan menyederhanakan penggunaan sedikit. Saya sarankan menambahkannya ke kelas PHPUnitUtil Anda seperti pada contoh di bawah ini atau ke PHPUnit_Framework_TestCase-kelas perluasan Anda (atau, saya kira, secara global ke file PHPUnitUtil Anda).

Sejak MyClass sedang instantiated lagian dan ReflectionClass dapat mengambil string atau objek ...

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
}

Saya juga membuat fungsi alias getProtectedMethod () untuk menjadi eksplisit apa yang diharapkan, tetapi itu terserah Anda.

Bersulang!

teastburn
sumber
+1 untuk menggunakan API Kelas Refleksi.
Bill Ortell
10

Saya pikir troelskn dekat. Saya akan melakukan ini sebagai gantinya:

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

Kemudian, terapkan sesuatu seperti ini:

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

Anda kemudian menjalankan tes Anda terhadap TestClassToTest.

Seharusnya dimungkinkan untuk secara otomatis menghasilkan kelas ekstensi tersebut dengan menguraikan kode. Saya tidak akan terkejut jika PHPUnit sudah menawarkan mekanisme seperti itu (meskipun saya belum memeriksa).

Michael Johnson
sumber
Heh ... sepertinya saya katakan, gunakan opsi ketiga Anda :)
Michael Johnson
2
Ya, itulah pilihan ketiga saya. Saya cukup yakin, bahwa PHPUnit tidak menawarkan mekanisme seperti itu.
GrGr
Ini tidak akan berfungsi, Anda tidak dapat mengganti fungsi yang dilindungi dengan fungsi publik dengan nama yang sama.
Koen.
Saya mungkin salah, tetapi saya tidak berpikir pendekatan ini bisa berhasil. PHPUnit (sejauh yang pernah saya gunakan) mengharuskan kelas pengujian Anda memperluas kelas lain yang menyediakan fungsionalitas pengujian yang sebenarnya. Kecuali ada jalan keluar yang saya tidak yakin saya bisa melihat bagaimana jawaban ini dapat digunakan. phpunit.de/manual/current/en/…
Cypher
FYI ini hanya berfungsi untuk metode yang dilindungi , bukan untuk yang pribadi
Sliq
5

Saya akan melemparkan topi saya ke atas ring di sini:

Saya telah menggunakan __call hack dengan tingkat keberhasilan yang beragam. Alternatif yang saya temukan adalah menggunakan pola Pengunjung:

1: menghasilkan stdClass atau kelas kustom (untuk menegakkan tipe)

2: prima dengan metode dan argumen yang diperlukan

3: memastikan bahwa SUT Anda memiliki metode acceptVisitor yang akan menjalankan metode dengan argumen yang ditentukan dalam kelas kunjungan

4: menyuntikkannya ke kelas yang ingin Anda uji

5: SUT menyuntikkan hasil operasi ke pengunjung

6: terapkan kondisi pengujian Anda ke atribut hasil Pengunjung

sunwukung
sumber
1
+1 untuk solusi menarik
jsh
5

Anda memang dapat menggunakan __call () secara umum untuk mengakses metode yang dilindungi. Untuk dapat menguji kelas ini

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

Anda membuat subkelas di 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);
    }
}

Perhatikan bahwa metode __call () tidak mereferensikan kelas dengan cara apa pun sehingga Anda dapat menyalin di atas untuk setiap kelas dengan metode yang dilindungi yang ingin Anda uji dan hanya mengubah deklarasi kelas. Anda mungkin dapat menempatkan fungsi ini di kelas dasar umum, tapi saya belum mencobanya.

Sekarang test case itu sendiri hanya berbeda di mana Anda membangun objek yang akan diuji, menukar di ExampleExposed for Example.

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

Saya percaya PHP 5.3 memungkinkan Anda untuk menggunakan refleksi untuk mengubah aksesibilitas metode secara langsung, tetapi saya berasumsi Anda harus melakukannya untuk setiap metode secara individual.

David Harkness
sumber
1
Implementasi __call () bekerja sangat baik! Saya mencoba memilih, tetapi saya membatalkan pilihan sampai setelah saya menguji metode ini dan sekarang saya tidak diperbolehkan memilih karena batas waktu dalam SO.
Adam Franco
The call_user_method_array()fungsi ditinggalkan sebagai PHP 4.1.0 ... menggunakan call_user_func_array(array($this, $method), $args)sebagai gantinya. Perhatikan bahwa jika Anda menggunakan PHP 5.3.2+ Anda dapat menggunakan Reflection untuk mendapatkan akses ke metode dan atribut yang dilindungi / pribadi
nuqqsa
@nuqqsa - Terima kasih, saya memperbarui jawaban saya. Sejak itu saya telah menulis Accessiblepaket generik yang menggunakan refleksi untuk memungkinkan tes untuk mengakses properti / metode pribadi kelas dan objek.
David Harkness
Kode ini tidak berfungsi untuk saya di PHP 5.2.7 - metode __call tidak dipanggil untuk metode yang didefinisikan oleh kelas dasar. Saya tidak dapat menemukannya didokumentasikan, tetapi saya menduga perilaku ini telah diubah di PHP 5.3 (di mana saya telah mengkonfirmasi itu berfungsi).
Russell Davis
@Russell - __call()hanya dipanggil jika pemanggil tidak memiliki akses ke metode. Karena kelas dan subkelasnya memiliki akses ke metode yang dilindungi, panggilan ke mereka tidak akan melalui __call(). Bisakah Anda memposting kode Anda yang tidak berfungsi di 5.2.7 dalam pertanyaan baru? Saya menggunakan hal di atas dalam 5.2 dan hanya pindah ke menggunakan refleksi dengan 5.3.2.
David Harkness
2

Saya menyarankan solusi berikut untuk solusi / ide "Henrik Paul" :)

Anda tahu nama metode pribadi kelas Anda. Misalnya mereka seperti _add (), _edit (), _delete () dll.

Oleh karena itu ketika Anda ingin mengujinya dari aspek unit-testing, panggil saja metode pribadi dengan awalan dan / atau suffix beberapa kata umum (misalnya _addPhpunit) sehingga ketika __call () metode dipanggil (karena metode _addPhpunit () tidak ada) dari kelas pemilik, Anda cukup memasukkan kode yang diperlukan dalam metode __call () untuk menghapus kata / s yang diawali / suffix (Phpunit) dan kemudian memanggil metode pribadi yang disimpulkan dari sana. Ini adalah penggunaan metode sihir yang bagus.

Cobalah.

Anirudh Zala
sumber