Cara menggunakan stub javascript

Ada dua gaya pengujian: "black box" dan "white box" style. Kotak hitam pengujian berfokus pada objek state, white box pengujian berfokus pada perilaku. Dua style saling melengkapi dan dapat dikombinasikan untuk benar-benar menguji kode. Mocking memungkinkan kita untuk menguji perilaku, dan tutorial ini menggabungkan konsep movking dengan TDD untuk membangun class contoh yang menggunakan beberapa komponen lain untuk mencapai tujuan.

Table of Contents

  • Langkah 1: Pengantar Behavior Testing
  • Langkah 2: Mainan mobil dikendalikan Remote
  • Langkah 3: Aplikasi skema
  • Langkah 4: Tes ganda
  • Langkah 5: Dummy objek
  • Langkah 6: "Stub" Status dan maju
  • Langkah 7: Menyelesaikan tes dengan Mock ujian sebenarnya
  • Langkah 8: Menggunakan Spy tes
  • Implementasi menggunakan Pure test spy
  • Kembali ke PHPUnit's Mocking Framework
  • Langkah 9: Tes Fake
  • Langkah 10: kesimpulan
  • Pustaka dan buku


Langkah 1: Pengantar Behavior Testing

Objek adalah entitas yang mengirim pesan ke satu sama lain. Setiap objek mengakui serangkaian pesan yang pada gilirannya jawaban. Ini adalah metode public pada objek. Metode private adalah kebalikan. Mereka benar-benar internal ke objek dan tidak dapat berkomunikasi dengan apa pun luar objek. Jika metode pjblic mirip dengan pesan, maka metode private mirip dengan pikiran.

Jumlah semua metode, publik dan private, dapat diakses melalui metode public mewakili perilaku objek. Misalnya, mengatakan objek bergerak menyebabkan objek untuk tidak hanya berinteraksi dengan metode yang internal, tetapi juga dengan objek lainnya. Dari sudut pandang pengguna, objek memiliki hanya satu perilaku sederhana: bergerak.

Dari programmer's point of view, namun, objek harus melakukan banyak hal-hal kecil untuk mencapai gerakan.

Sebagai contoh, bayangkan objek mobil. Dalam rangka untuk itu untuk bergerak, ia harus memiliki mesin berjalan, gigi (atau sebaliknya), dan roda untuk bergerak. Ini adalah perilaku yang kita perlu untuk menguji dan membangun untuk merancang dan menulis kode produksi kami.


Langkah 2: Mainan mobil dikendalikan Remote

class yang kami uji tidak pernah benar-benar menggunakan objek-objek ini dummy.

Mari kita bayangkan kita sedang membangun sebuah program untuk remote control mobil mainan. Semua perintah class kami datang melalui remote control. Kita harus membuat sebuah class yang memahami apa yang remote control mengirim dan isu-isu perintah ke mobil.

Ini akan menjadi sebuah aplikasi latihan, dan kami menganggap bahwa class lain yang mengendalikan berbagai bagian dari mobil sudah tertulis. Kita tahu tanda-tangan yang tepat dari class-class ini, tapi sayangnya, produsen mobil tidak bisa mengirim kami sebuah prototipe--tidak bahkan source code. Semua kita tahu adalah nama-nama class, metode yang mereka miliki, dan perilaku apa setiap metode yang dipunyai. Nilai-nilai kembalian juga ditentukan.


Langkah 3: Aplikasi skema

Berikut adalah skema lengkap aplikasi. tidak ada penjelasan pada titik ini; hanya perlu diingat.

Cara menggunakan stub javascript


Langkah 4: Tes ganda

Tes stub adalah objek untuk mengontrol input langsung kode yang diuji.

Mocking adalah gaya pengujian yang memerlukan seperangkat tpp;, seperangkat objek-objek khusus yang mewakili tingkat yang berbeda dari faking perilaku objek. Ini adalah:

  • Objek Dummy
  • test stub
  • test spies
  • test mocks
  • tes fale

Setiap dari objek-objek ini memiliki lingkup khusus dan perilaku mereka. Dalam PHPUnit, mereka diciptakan dengan metode $this->getMock(). Perbedaannya adalah cara dan alasan apa objek ini digunakan.

Untuk lebih memahami objek-objek ini, saya akan mengimplementasikan "Mainan mobil Controller" langkah demi langkah menggunakan jenis objek ini, dalam rangka, seperti yang tercantum di atas. Setiap objek dalam daftar lebih kompleks daripada objek sebelum itu. Ini mengarah ke sebuah implementasi yang secara radikal berbeda dalam dunia nyata. Juga, menjadi sebuah aplikasi khayalan, saya akan menggunakan beberapa skenario yang bahkan mungkin tidak layak dalam mobil mainan asli. Tapi hei, mari kita bayangkan apa yang kita perlu mereka untuk memahami gambaran yang lebih besar.


Langkah 5: Dummy objek

Dummy objek adalah objek yang System Under Test (SUT) tergantung pada, tapi mereka benar-benar tidak pernah digunakan. Sebuah objek dummy dapat argumen yang dimasukan ke objek lain, atau dapat dikembalikan oleh objek kedua dan kemudian berlalu untuk objek ketiga. Intinya adalah, class yang kami uji tidak pernah benar-benar menggunakan objek-objek dummy. Pada saat yang sama, objek harus menyerupai objek asli; Sebaliknya, Penerima dapat menolak itu.

Cara terbaik untuk menunjukkan hal ini adalah membayangkan skenario; skema yang, di bawah ini:

Orange obyek adalah RemoteControlTranslator. Tujuan utama adalah untuk menerima sinyal dari remote control dan menerjemahkannya ke dalam pesan untuk class-class kami. Di beberapa titik, pengguna akan melakukan tindakan yang "Ready to Go" pada remote control. Penerjemah akan menerima pesan dan menciptakan class yang diperlukan untuk membuat mobil yang siap untuk dipakai.

Produsen mengatakan bahwa "Ready to Go" berarti bahwa mesin dimulai, gearbox di netral, dan lampu diatur ke hidup atau mati sesuai permintaan pengguna.

Ini berarti bahwa pengguna dapat Tentukan dulu kata keadaan lampu sebelum siap untuk pergi, dan mereka akan mengaktifkan atau menonaktifkan berdasarkan nilai ini standar pada aktivasi. RemoteControlTranslator kemudian mengirimkan semua informasi yang diperlukan untuk class CarControl getReadyToGo ($engine, $gearbox, $electronics, $lights) metode. Saya tahu ini jauh dari desain yang sempurna dan melanggar beberapa prinsip dan patern, tapi hal ini sangat baik untuk contoh ini.

Mulai proyek kami dengan struktur file awal ini:

Ingat, semua class di CarInterface folder disediakan oleh produsen mobil; kita tidak tahu implementasinya. Semua kita tahu class signature, tetapi kita tidak peduli tentang mereka saat ini.

Tujuan utama kami adalah untuk menerapkan class CarController. Untuk menguji class ini, kita perlu membayangkan bagaimana kita ingin menggunakannya. Dengan kata lain, kita menempatkan diri dalam sepatu RemoteControlTranslator dan/atau kelas masa depan lainnya yang dapat menggunakan CarController. Mari kita mulai dengan membuat kasus untuk class kami.

class CarControllerTest extends PHPUnit_Framework_TestCase {
}

Kemudian tambahkan sebuah metode pengujian.

  function testItCanGetReadyTheCar() {
	}

Sekarang pikirkan apa yang kita butuhkan untuk passing ke metode getReadyToGo(): mesin, gearbox, controller elektronik dan informasi ringan. Untuk contoh ini, kita hanya akan mocking lampu:

require_once '../CarController.php';
include '../autoloadCarInterfaces.php';

class CarControllerTest extends PHPUnit_Framework_TestCase {

	function testItCanGetReadyTheCar() {
		$carController = new CarController();

		$engine = new Engine();
		$gearbox = new Gearbox();
		$electornics = new Electronics();

		$dummyLights = $this->getMock('Lights');

		$this->assertTrue($carController->getReadyToGo($engine, $gearbox, $electornics, $dummyLights));
	}

}

Hal ini jelas akan gagal dengan:

PHP Fatal error:  Call to undefined method CarController::getReadyToGo()

Meskipun gagal, tes ini memberikan titik awal untuk pelaksanaan CarController kami. Saya menyertakan file, bernama autoloadCarInterfaces.php, yang tidak dalam ada di daftar awal. Aku sadar aku butuh sesuatu untuk memuat class, dan saya menulis sebuah solusi yang sangat dasar. Kami selalu dapat menulisnya ketika class asli yang disediakan, tapi itu cerita yang sama sekali berbeda. Untuk sekarang, kita akan tetap dengan solusi yang mudah:

foreach (scandir(dirname(__FILE__) . '/CarInterface') as $filename) {
	$path = dirname(__FILE__) . '/CarInterface/' . $filename;

	if (is_file($path)) {
		require_once $path;
	}
}

Saya menganggap loader class ini jelas bagi semua orang; Jadi, mari kita bahas kode tes.

Pertama, kita membuat sebuah instance dari CarController, class kami ingin uji. Selanjutnya, kita membuat instance dari semua class yang kita peduli: mesin, gearbox dan elektronik.

Kami kemudian membuat objek Lights dummy dengan memanggil PHPUnit's getMock() metode dan memasukan nama class Lights. Ini mengembalikan instance Lights, tapi setiap metode mengembalikan null--sebuah objek dummy. Objek dummy tidak bisa melakukan apapun, tetapi memberikan kode kita interface yang perlu bekerja dengan objek Light.

Hal ini sangat penting untuk dicatat bahwa $dummyLights adalah objek Light, dan setiap pengguna yang mengharapkan sebuah objek Light dapat menggunakan objek dummy tanpa mengetahui bahwa itu bukan objek Light asku.

Untuk menghindari kebingungan, saya merekomendasikan menetapkan parameter jenis ketika menentukan fungsi. Hal ini akan memaksa runtime PHP untuk Periksa jenis argumen yang dimasukan ke fungsi. Tanpa menentukan tipe data, Anda dapat melewati setiap objek ke setiap parameter, yang dapat mengakibatkan kegagalan kode Anda. Dengan ini dalam pikiran, mari kita periksa class Electronics:

require_once 'Lights.php';

class Electronics {

	function turnOn(Lights $lights) {}

}

Mari kita implementasi tes:

class CarController {

	function getReadyToGo(Engine $engine, Gearbox $gearbox, Electronics $electronics, Lights $lights) {
		$engine->start();
		$gearbox->shift('N');

		$electronics->turnOn($lights);

		return true;
	}

}

Seperti yang Anda lihat, fungsi getReadyToGo() digunakan $lights objek untuk tujuan hanya mengirim ke objek $electronics turnOn() metode. Apakah ini solusi ideal untuk situasi seperti ini? Mungkin tidak, tapi jelas Anda dapat mengamati bagaimana sebuah objek dummy, dengan tidak ada hubungan apapun ke fungsi getReadyToGo(), adalah diteruskan kepada satu objek yang benar-benar membutuhkannya.

Harap dicatat bahwa semua kelas yang terkandung dalam direktori CarInterface menyediakan objek dummy ketika diinisialisasi. Juga menganggap bahwa, untuk latihan ini, kita mengharapkan produsen untuk memberikan class asli di masa depan. Kita tidak bisa mengandalkan kurangnya mereka saat ini fungsi; Jadi, kita harus memastikan bahwa kami tes sukses.


Langkah 6: "Stub" Status dan maju

Tes Stub adalah objek untuk mengontrol input langsung kode yang diuji. Tapi apa itu indirect input? Ini adalah sumber informasi yang dapat tidak secara langsung ditentukan.

Contoh paling umum dari sebuah stub uji adalah ketika objek objek lain meminta informasi dan kemudian melakukan sesuatu dengan data tersebut.

Spies, menurut definisi, adalah lebih mampu stub.

Data hanya bisa diperoleh oleh meminta objek tertentu untuk itu, dan dalam banyak kasus, objek yang digunakan untuk tujuan tertentu di dalam class yang diuji. Kami tidak ingin "baru up" (new SomeClass()) class dalam class yang lain untuk tujuan pengujian. Oleh karena itu, kita perlu menyuntikkan instance class yang bertindak seperti SomeClass tanpa injecting objek SomeClass yang sebenarnya.

Apa yang kita inginkan adalah stub class, yang kemudian menyebabkan dependency injection. Dependency Injection (DI) adalah teknik yang menyuntikkan objek ke objek lain, memaksanya untuk menggunakan objek yang diinject DI umum di TDD, dan itu benar-benar diperlukan dalam hampir setiap proyek. ini menyediakan cara yang mudah untuk memaksa objek untuk menggunakan class tes-siap daripada class asli yang digunakan dalam lingkungan produksi.

Mari kita membuat Mobil Mainan kami bergerak maju.

Kami ingin menerapkan metode yang disebut moveForward(). Metode ini pertama query sebuah objek StatusPanel untuk bahan bakar dan mesin status. Jika mobil siap untuk pergi, kemudian metode memerintahkan elektronik untuk mempercepat.

Untuk lebih memahami bagaimana sebuah stub bekerja, saya akan pertama kali menulis kode untuk status yang memeriksa dan percepatan:

	function goForward(Electronics $electronics) {
		$statusPanel = new StatusPanel();
		if($statusPanel->engineIsRunning() && $statusPanel->thereIsEnoughFuel())
			$electronics->accelerate ();
	}

Kode ini cukup sederhana, tetapi kita tidak punya mesin nyata atau bahan bakar untuk menguji pelaksanaan goForward() kami. Kode kita bahkan tidak akan memasukan if statement karena kita tidak memiliki class StatusPanel. Tetapi jika kita terus dengan tes, solusi Logis mulai muncul:

	function testItCanAccelerate() {
		$carController = new CarController();

		$electronics = new Electronics();

		$stubStatusPanel = $this->getMock('StatusPanel');
		$stubStatusPanel->expects($this->any())->method('thereIsEnoughFuel')->will($this->returnValue(TRUE));
		$stubStatusPanel->expects($this->any())->method('engineIsRunning')->will($this->returnValue(TRUE));

		$carController->goForward($electronics, $stubStatusPanel);
	}

baris demi baris penjelasan:

Saya suka rekursi; selalu lebih mudah untuk menguji rekursi daripada loop.

  • menciptakan CarController baru
  • membuat objek Electronics tergantung
  • membuat mock untuk StatusPanel
  • berharap untuk memanggil thereIsEnoughFuel() nol atau lebih kali dan mengembalikan true
  • berharap untuk memanggil engineIsRunning() nol atau lebih kali dan mengembalikan true
  • memanggil goForward() dengan objek Elektronics dan StubbedStatusPanel

Ini adalah tes yang kita ingin menulis, tetapi itu tidak akan bekerja dengan implementasi goForward() kami saat ini. Kita harus memodifikasi:

	function goForward(Electronics $electronics, StatusPanel $statusPanel = null) {
		$statusPanel = $statusPanel ? : new StatusPanel();
		if($statusPanel->engineIsRunning() && $statusPanel->thereIsEnoughFuel())
			$electronics->accelerate ();
	}

Modifikasi kami menggunakan dependency injection dengan menambahkan parameter opsional kedua jenis StatusPanel. Kami menentukan jika parameter ini memiliki nilai dan menciptakan StatusPanel baru jika $statusPanel null. Hal ini memastikan bahwa objek StatusPanel baru dibuat dalam produksi sementara masih memungkinkan kita untuk menguji metode.

Sangat penting untuk menentukan jenis $statusPanel parameter. Hal ini memastikan bahwa hanya sebuah objek StatusPanel (atau object dari class yang mewarisi) dapat dilewatkan ke metode. Tetapi bahkan dengan modifikasi ini, pengujian kami masih belum lengkap.


Langkah 7: Menyelesaikan tes dengan Mock ujian sebenarnya

Kita harus menguji mock objek Electronics untuk memastikan kami metode dari langkah 6 memangil accelerate(). Kita tidak dapat menggunakan Electronics class asli karena beberapa alasan:

  • Kami tidak memiliki class.
  • Kami tidak dapat memverifikasi perilaku.
  • Bahkan jika kita bisa memaggilnya, kita harus mengujinya dalam isolasi.

Mock test adalah objek yang mampu mengendalikan langsung input dan output, dan memiliki mekanisme untuk otomatis assertion pada ekpetasi dan hasil. Definisi ini mungkin terdengar agak membingungkan, tapi benar-benar cukup sederhana untuk diterapkan:

	function testItCanAccelerate() {
		$carController = new CarController();

		$electronics = $this->getMock('Electronics');
		$electronics->expects($this->once())->method('accelerate');

		$stubStatusPanel = $this->getMock('StatusPanel');
		$stubStatusPanel->expects($this->any())->method('thereIsEnoughFuel')->will($this->returnValue(TRUE));
		$stubStatusPanel->expects($this->any())->method('engineIsRunning')->will($this->returnValue(TRUE));

		$carController->goForward($electronics, $stubStatusPanel);
	}

Kami hanya mengubah variabel $electronics. Alih-alih membuat objek Electronics asli, kami hanya membuat mock.

Di baris berikutnya, kita mendefinisikan sebuah expectation pada objek $electronics. Lebih tepatnya, kami berharap bahwa metode accelerate() dipanggil hanya satu kali ($this-> once()). tes sekarang sukses!

Merasa bebas untuk bermain-main dengan tes ini. Mencoba mengubah $this->once() menjadi $this->exactly(2) dan melihat apa pesan kegagalan bagus PHPUnit memberikan kepada Anda:

1) CarControllerTest::testItCanAccelerate
Expectation failed for method name is equal to ; when invoked 2 time(s).
Method was expected to be called 2 times, actually called 1 times.

Langkah 8: Menggunakan Spy tes

Tes spy adalah sebuah objek yang mampu menangkap output tidak langsung dan memberikan langsung masukan yang diperlukan.

indirect output adalah sesuatu yang kita tidak bisa secara langsung mengamati. Sebagai contoh: ketika kelas diuji menghitung nilai dan kemudian menggunakannya sebagai argumen untuk metode objek lain. Satu-satunya cara untuk mengamati output ini adalah untuk menanyakan objek dipanggil variabel yang digunakan untuk mengakses metode.

Definisi ini membuat spy hampir seperti mock.

Perbedaan utama antara mock dan spy adalah bahwa mock objek memiliki built-in assertion dan expectation.

Dalam hal ini, bagaimana kita dapat membuat tes spy menggunakan PHPUnit's getMock()? Kita tidak bisa (Yah, kita tidak bisa membuatspy murni), tetapi kita dapat buat mock yang mampu memata-matai objek lainnya.

Mari kita menerapkan sistem pengereman sehingga kita dapat menghentikan mobil. Pengereman benar-benar sederhana; remote control akan merasakan pengereman intensitas dari pengguna dan mengirimkannya ke controller. Remote juga menyediakan "Emergency Stop!" tombol. Ini harus langsung terlibat rem dengan daya maksimum.

Kekuatan pengereman mengukur nilai-nilai yang berkisar dari 0 hingga 100, dengan 0 berarti apa-apa dan 100 berarti daya maksimum rem. Perintah "Emergency Stop!" akan diterima sebagai panggilan yang berbeda.

CarController akan mengeluarkan pesan ke objek Electronics untuk mengaktifkan sistem rem. Controller mobil juga dapat permintaan StatusPanel untuk kecepatan informasi yang diperoleh melalui sensor pada mobil.

Implementasi menggunakan Pure test spy

Mari kita pertama kali menerapkan sebuah objek spy murni tanpa menggunakan PHPUnit's mocking infrastruktur. Ini akan memberi Anda pemahaman yang lebih baik dari konsep spy tes. Kita mulai dengan memeriksa signature Electronics objek.

class Electronics {

	function turnOn(Lights $lights) {}
	function accelerate(){}
	function pushBrakes($brakingPower){}

}

Kami tertarik dalam metode pushBrakes(). Aku tidak menyebutnya brake() untuk menghindari kebingungan dengan kata kunci break di PHP.

Untuk membuat spy yang nyata, kita akan extend Electronics dan meng-override metode pushBrakes(). Metode ditimpa ini tidak akan mendorong rem; Sebaliknya, itu hanya akan mendaftarkan kekuatan pengereman.

class SpyingElectronics extends Electronics {
	private $brakingPower;

	function pushBrakes($brakingPower) {
		$this->brakingPower = $brakingPower;
	}

	function getBrakingPower() {
		return $this->brakingPower;
	}
}

Metode getBrakingPower() yang memberikan kita kemampuan untuk memeriksa kekuatan pengereman dalam pengujian kami. Ini bukanlah metode yang akan digunakan dalam produksi.

Kita sekarang dapat menulis tes mampu menguji kekuatan pengereman. Mengikuti prinsip-prinsip TDD, kita akan mulai dengan ujian sederhana dan menyediakan implementasi paling dasar:

	function testItCanStop() {
		$halfBrakingPower = 50;
		$electronicsSpy = new SpyingElectronics();

		$carController = new CarController();
		$carController->pushBrakes($halfBrakingPower, $electronicsSpy);

		$this->assertEquals($halfBrakingPower, $electronicsSpy->getBrakingPower());
	}

Tes ini gagal karena kita belum memiliki metode pushBrakes() pada CarController. Mari kita memperbaiki dan menulisnya:

	function pushBrakes($brakingPower, Electronics $electronics) {
		$electronics->pushBrakes($brakingPower);
	}

Ujian sekarang berlalu, efektif pengujian metode pushBrakes().

Kami juga dapat memata-matai pemanggilan metode. Pengujian clas StatusPanel adalah langkah logis berikutnya. Ini menyediakan pengguna potongan-potongan yang berbeda informasi tentang mobil dikendalikan remote. Mari kita menulis tes yang memeriksa jika objek StatusPanel ditanya tentang kecepatan mobil. Kami akan membuat spy untuk itu:

class SpyingStatusPanel extends StatusPanel {
	private $speedWasRequested = false;

	function getSpeed() {
		$this->speedWasRequested = true;
	}

	function speedWasRequested() {
		return $this->speedWasRequested;
	}
}

Kemudian, kami mengubah kami tes untuk menggunakan spy:

	function testItCanStop() {
		$halfBrakingPower = 50;
		$electronicsSpy = new SpyingElectronics();
		$statusPanelSpy = new SpyingStatusPanel();

		$carController = new CarController();
		$carController->pushBrakes($halfBrakingPower, $electronicsSpy, $statusPanelSpy);

		$this->assertEquals($halfBrakingPower, $electronicsSpy->getBrakingPower());
		$this->assertTrue($statusPanelSpy->speedWasRequested());
	}

Perhatikan bahwa saya tidak menulis tes terpisah.

Rekomendasi "satu assertion per tes" baik untuk mengikuti, tapi ketika pengujian Anda menjelaskan tindakan yang memerlukan beberapa langkah atau state, menggunakan lebih dari satu assertion dalam tes yang sama dapat diterima.

Bahkan lebih, hal ini membuat pernyataan Anda tentang satu konsep di satu tempat. Ini membantu menghilangkan duplikasi kode dengan tidak mengharuskan Anda untuk berulang kali mengatur kondisi yang sama untuk SUT Anda.

Dan sekarang implementasi:

	function pushBrakes($brakingPower, Electronics $electronics, StatusPanel $statusPanel = null) {
		$statusPanel = $statusPanel ? : new StatusPanel();
		$electronics->pushBrakes($brakingPower);
		$statusPanel->getSpeed();
	}

Ada hanya hal kecil, kecil yang mengganggu saya: nama tes ini adalah testItCanStop(). Yang jelas menyiratkan bahwa kami mendorong rem sampai mobil datang benar-benar berhenti. Kami, namun, memanggil pushBrakes() metode, yang tidak tepat. Waktu untuk refactor:

	function stop($brakingPower, Electronics $electronics, StatusPanel $statusPanel = null) {
		$statusPanel = $statusPanel ? : new StatusPanel();
		$electronics->pushBrakes($brakingPower);
		$statusPanel->getSpeed();
	}

Jangan lupa untuk mengubah metode panggilan dalam tes juga.

$carController->stop($halfBrakingPower, $electronicsSpy, $statusPanelSpy);

Indirect output adalah sesuatu yang kita tidak bisa secara langsung mengamati.

Pada titik ini, kita perlu berpikir tentang sistem pengereman dan bagaimana cara kerjanya. Ada beberapa kemungkinan, tetapi untuk contoh ini, menganggap bahwa penyedia Mobil Mainan ditentukan bahwa pengereman terjadi dalam interval. Memanggil metode pushBreakes() objek Electronics mendorong rem untuk jumlah waktu yang dan kemudian melepaskan itu. Interval waktu tidak penting bagi kita, tetapi mari kita bayangkan itu sepersekian detik. Dengan interval waktu kecil seperti itu, kita harus terus mengirim perintah pushBrakes() sampai kecepatan adalah nol.

Spy, menurut definisi, adalah lebih mampu dari stib, dan mereka juga dapat mengontrol langsung masukan jika diperlukan. Mari kita membuat spy StatusPanel kami lebih mampu dan menawarkan beberapa nilai untuk kecepatan. Saya pikir panggilan pertama harus memberikan kecepatan positif - Mari kita mengatakan nilai 1. Panggilan kedua akan memberikan kecepatan 0.

class SpyingStatusPanel extends StatusPanel {

	private $speedWasRequested = false;
	private $currentSpeed = 1;

	function getSpeed() {
		if ($this->speedWasRequested) $this->currentSpeed = 0;
		$this->speedWasRequested = true;
		return $this->currentSpeed;
	}

	function speedWasRequested() {
		return $this->speedWasRequested;
	}

	function spyOnSpeed() {
		return $this->currentSpeed;
	}

}

getSpeed() yang ditimpa metode mengembalikan nilai kecepatan melalui metode spyOnSpeed(). Mari kita tambahkan assertion ketiga untuk pengujian kami:

	function testItCanStop() {
		$halfBrakingPower = 50;
		$electronicsSpy = new SpyingElectronics();
		$statusPanelSpy = new SpyingStatusPanel();

		$carController = new CarController();
		$carController->stop($halfBrakingPower, $electronicsSpy, $statusPanelSpy);

		$this->assertEquals($halfBrakingPower, $electronicsSpy->getBrakingPower());
		$this->assertTrue($statusPanelSpy->speedWasRequested());
		$this->assertEquals(0, $statusPanelSpy->spyOnSpeed());
	}

Menurut pernyataan terakhir, kecepatan harus memiliki nilai kecepatan 0 setelah metode stop() selesai eksekusi. Menjalankan tes ini terhadap produksi kami hasil kode kegagalan dengan pesan samar:

1) CarControllerTest::testItCanStop
Failed asserting that 1 matches expected 0.

Mari kita tambahkan pesan kustom assertion kita sendiri:

$this->assertEquals(0, $statusPanelSpy->spyOnSpeed(),
	'Expected speed to be 0 (zero) after stopping but it actually was ' . $statusPanelSpy->spyOnSpeed());

Yang menghasilkan sebuah pesan kegagalan yang jauh lebih mudah dibaca:

1) CarControllerTest::testItCanStop
Expected speed to be 0 (zero) after stopping but it actually was 1
Failed asserting that 1 matches expected 0.

Kegagalan cukup! Mari kita membuat hal itu sukses.

	function stop($brakingPower, Electronics $electronics, StatusPanel $statusPanel = null) {
		$statusPanel = $statusPanel ? : new StatusPanel();
		$electronics->pushBrakes($brakingPower);
		if ($statusPanel->getSpeed()) $this->stop($brakingPower, $electronics, $statusPanel);
	}

Saya suka rekursi; selalu lebih mudah untuk menguji rekursi daripada loop. Mudah menguji kode sederhana berarti, yang pada gilirannya berarti algoritma yang lebih baik. Check out transformasi prioritas premis untuk lebih lanjut tentang subjek ini.

Kembali ke PHPUnit's Mocking Framework

Cukup dengan class tambahan. Mari kita menulis ulang ini menggunakan PHPUnit's mocking framework dan menghilangkan spy yang murni. Mengapa?

Karena PHPUnit menawarkan sintaks mocking yang lebih baik dan lebih sederhana, lebih sedikit kode dan beberapa metode standar yang bagus.

Saya biasanya membuat spy murni dan stub hanya ketika mocking mereka dengan getMock() akan terlalu rumit. Jika kelas Anda begitu kompleks getMock() yang tidak bisa menangani mereka, maka Anda memiliki masalah dengan kode produksi--tidak dengan Anda tes.

	function testItCanStop() {
		$halfBrakingPower = 50;
		$electronicsSpy = $this->getMock('Electronics');
		$electronicsSpy->expects($this->exactly(2))->method('pushBrakes')->with($halfBrakingPower);
		$statusPanelSpy = $this->getMock('StatusPanel');
		$statusPanelSpy->expects($this->at(0))->method('getSpeed')->will($this->returnValue(1));
		$statusPanelSpy->expects($this->at(1))->method('getSpeed')->will($this->returnValue(0));

		$carController = new CarController();
		$carController->stop($halfBrakingPower, $electronicsSpy, $statusPanelSpy);
	}

Jumlah semua metode, publik dan private, dapat diakses melalui metode umum mewakili perilaku objek.

Penjelasan baris demi baris kode di atas:

  • mengatur kekuatan pengereman setengah = 50
  • membuat mock Electronics
  • Setiap metode pushBrakes() untuk mengeksekusi tepat dua kali dengan di atas ditentukan pengereman daya
  • membuat mock StatusPanel
  • mengembalikan 1 pada panggilan pertama getSpeed()
  • mengembalikan 0 pada eksekusi kedua getSpeed()
  • memanggil metode stop() diuji pada objek CarController asli

Mungkin hal yang paling menarik dalam kode ini adalah metode $this->at($someValue). PHPUnit menghitung jumlah panggilan ke mock itu. Menghitung terjadi pada tingkat mock; Jadi, memanggil beberapa metode $statusPanelSpy akan menaikan counter. Ini mungkin tampak agak kontra-intuitif pada mulanya; Jadi mari kita lihat sebuah contoh.

Menganggap kita ingin memeriksa tingkat bahan bakar pada masing-masing panggilan untuk stop(). Kode akan terlihat seperti ini:

	function stop($brakingPower, Electronics $electronics, StatusPanel $statusPanel = null) {
		$statusPanel = $statusPanel ? : new StatusPanel();
		$electronics->pushBrakes($brakingPower);
		$statusPanel->thereIsEnoughFuel();
		if ($statusPanel->getSpeed()) $this->stop($brakingPower, $electronics, $statusPanel);
	}

Ini akan menghancurkan pengujian kami. Anda mungkin bingung mengapa, tetapi Anda akan mendapatkan pesan berikut:

1) CarControllerTest::testItCanStop
Expectation failed for method name is equal to  when invoked 2 time(s).
Method was expected to be called 2 times, actually called 1 times.

Sudah cukup jelas pushBrakes() yang harus dipanggil dua kali. Mengapa kemudian kita mendapatkan pesan ini? Karena expectation $this->at($someValue). Konter akan bertambah sebagai berikut:

  • panggilan pertama untuk stop()-> pertama panggilan untuk thereIsEnougFuel() => internal counter di 0
  • panggilan pertama untuk stop()-> pertama panggilan untuk getSpeed() => internal counter 1 dan mengembalikan 0
  • panggilan kedua untuk stop() tidak pernah terjadi => panggilan kedua untuk getSpeed() tidak pernah terjadi

Masing-masing panggilan setiap metode mock pada $statusPanelSpy akan menambahkan counter internal PHPUnit's.


Langkah 9: Tes Fake

Jika metode public mirip dengan pesan, maka metode private mirip dengan pikiran.

Tes fake adalah implementasi sederhana dari sebuah objek kode produksi. Ini adalah definisi yang sangat mirip untuk menguji di tes stub. Pada kenyataannya, Fake dan stub sangat mirip seperti perilaku eksternal. Keduanya objek yang meniru perilaku benda nyata lain, dan keduanya menerapkan metode untuk mengontrol input tidak langsung. Perbedaannya adalah bahwa Fake jauh lebih dekat dengan objek asli daripada ke objek dummy.

Sebuah stub pada dasarnya adalah sebuah dummy objek yang metodenya mengembalikan nilai-nilai yang telah ditetapkan. Fake, namun, melakukan implementasi objek nyata yang lengkap dalam cara yang jauh lebih sederhana. Mungkin contoh yang paling sering adalah InMemoryDatabase untuk sempurna mensimulasikan kelas asli database tanpa benar-benar menulis untuk penyimpanan data. Dengan demikian, pengujian menjadi lebih cepat.

Tes Fake tidak harus menerapkan metode apapun untuk langsung mengendalikan masukan atau mengembalikan state yang diamati. Mereka tidak digunakan dipertanyakan; mereka digunakan untuk menyediakan--tidak diamati. Kasus penggunaan paling umum Fake adalah ketika tergantung pada komponen aslu (DOC) bukanlah namun ditulis, itu terlalu lambat (seperti database), atau DOC nyata tidak tersedia di lingkungan pengujian.


Langkah 10: kesimpulan

Fungsi pura-pura yang paling penting adalah mengendalikan DOC. Ini juga menyediakan cara yang bagus untuk mengontrol indirect I/O dengan bantuan teknik dependency injection.

Ada dua pendapat utama mengenai mocking:

Beberapa mengatakan bahwa mocking buruk...

  • Beberapa orang mengatakan bahwa mocling buruk, dan mereka benar. Mocking melakukan sesuatu yang jelek: ini berikatan terlalu banyak tes untuk implementasi. Bila mungkin, tes harus sebagai independen mengenai implementasi sebanyak mungkin. Black Box pengujian ini selalu lebih baik untuk whote box pengujian. Selalu menguji state jika Anda bisa; tidak mock behavior. Anti-mockist yang mendorong bawah-atas pembangunan dan desain. Ini berarti bahwa komponen-komponen kecil sistem diciptakan pertama dan kemudian menyatu menjadi struktur yang harmonis.
  • Beberapa orang mengatakan bahwa mocking itu bagus, dan mereka benar. Mengejek melakukan sesuatu yang yang indah; itu mendefinisikan perilaku. Itu membuat kita berpikir lebih dari sudut pandang pengguna. Mockists biasanya menggunakan pendekatan top-down untuk implementasi dan desain. Mereka mulai dengan class paling dalam sistem dan menulis tes pertama dengan mocking beberapa DOC imajiner lainnya yang belum dilaksanakan. Komponen sistem muncul dan berkembang berdasarkan mock yang dibuat pada satu tingkat lebih tinggi.

Ini dikatakan, itu terserah Anda untuk memutuskan mana cara untuk dipakai.

Beberapa lebih suka menjadi mockists sementara orang lain lebih suka state pengujian. Masing-masing pendekatan memiliki pro dan kontra. Sistem mocked semua menawarkan informasi tambahan perilaku dalam tes. Sistem all-state menawarkan lebih detail tentang komponen, tetapi juga dapat menyembunyikan beberapa perilaku.


Pustaka dan buku

  • PHPUnit resmi mocking dokumentasi
  • Mock bukan stub: sebuah posting blog oleh Martin Fowler
  • Transformasi prioritas premis blog posting oleh Robert C. Martin
  • xUnit Test Pattern: tes Refactoring kode buku oleh Gerard Meszaros
  • Bekerja secara efektif dengan kode warisan sebuah buku oleh Michael Feather