Menggunakan Mocks dan Rintisan di PHPUnit

Posted on

[ad_1]

Di kolom sebelumnya pengujian unit PHP dengan PHPUnit, saya menunjukkan cara menyiapkan PHPUnit dan cara menjalankan beberapa pengujian sederhana. Anda seharusnya sekarang dapat menguji fungsi atau metode apa pun yang tidak bergantung pada pemanggilan metode atau fungsi lain – yang jarang terjadi, karena sebagian besar program adalah campuran kompleks dari metode dan, dalam kasus PHP, fungsi.

Pengarang: Kendrick Curtis, Perangkat Lunak Stainless, http://www.stainless-software.com/

Tes unit penulisan untuk aplikasi apa pun bergantung pada kemampuan untuk mengisolasi objek yang diuji, yang dapat menjadi sulit dalam situasi ini. Di sini kita melihat beberapa strategi untuk mengisolasi kode yang ingin Anda uji dengan benar untuk membuktikannya berfungsi tanpa harus menguji seluruh sistem Anda.

Tergantung pada apa yang Anda baca, perbedaan yang berbeda dibuat antara benda tiruan, benda palsu, dan kebijaksanaan. PHPUnit secara native menyediakan objek dummy dengan fungsionalitas yang cukup untuk memberikan input yang telah ditentukan sebelumnya ke kode yang sedang diuji, serta untuk memverifikasi apakah metode tertentu telah dipanggil.

PHPUnit

Injeksi input dengan objek dummy

Sederhananya, objek tiruan adalah versi “palsu” dari objek kode (biasanya objek dalam pengertian OO juga). Untuk mengisolasi objek pengujian Anda, biasanya perlu untuk mengejek semua objek lain yang berinteraksi dengannya. Objek tiruan memiliki antarmuka yang sama dengan aslinya, tetapi merespons dengan cara yang telah diprogram sebelumnya oleh kode pengujian Anda. Mari kita lihat sebuah contoh, kelas DataProcessor sederhana yang menggunakan DataReader dan melakukan operasi sederhana:

class DataReader
{
public function readData()
{
$json = file_get_contents("http://stainless-software.com/stm/json");
return $json;
}
}

class DataProcessor
{
public function process(DataReader $dr)
{
$json_data = $dr->readData();
$data = json_decode($json_data);
return $data;
}
}

Untuk menguji DataProcessor, kami tidak ingin mengkhawatirkan kinerja DataReader. Kami akan mengujinya secara terpisah. Jadi, alih-alih menyediakan DataReader yang sebenarnya, kami malah menyediakan model dummy yang siap memberikan nilai konstan:

class DataProcessorTest extends PHPUnit_Framework_TestCase
{
function testDataProcessorProcess()
{
$mock_dr = $this->getMock("DataReader");
$mock_dr->expects($this->any())
->method("readData")
->will($this->returnValue('{"data": [4,5,6]}'));

$dp = new DataProcessor();
$result = $dp->process($mock_dr);
$this->assertInstanceOf("stdClass", $result);
$this->assertCount(3, $result->data);
$this->assertEquals(4, $result->data[0]);
$this->assertEquals(5, $result->data[1]);
$this->assertEquals(6, $result->data[2]);
}
}

Metode getMock PHPUnit_Framework_TestCase memungkinkan kita membuat versi tiruan dari kelas nyata dengan mudah. Seperti yang ditunjukkan, kami mengonfigurasi Mac dengan gaya yang lancar. Kami ingin mengejek metode “readData” untuk mengembalikan hasil seperti yang diberikan alih-alih mengandalkan kode aktual:

* -> Diharapkan: Ini membutuhkan kecocokan yang terkubur dalam manual [1]. $this->any() berarti Mac mengharapkan banyak panggilan ke metode yang ingin kita tentukan. Nanti kita akan melihat cara menggunakan ini untuk tujuan validasi pemanggilan metode.
* ->Metode: Nama adalah metode untuk mengejek.
* ->akan: adalah apa yang dikembalikan metode ini kepada pengguna saat dipanggil.

Mengejek objek “baru”.

Sejauh ini sangat sederhana. Salah satu masalah paling umum yang akan Anda temui dengan tiruan adalah bahwa kode yang diuji lebih terlihat seperti ini:

class DataProcessor
{
public function process()
{
$dr = new DataReader();
$json_data = $dr->readData();
$data = json_decode($json_data);
return $data;
}
}

Sangat tidak mungkin untuk mengganti $dr itu dengan tiruan karena objek baru dibuat dalam metode yang diuji. Alih-alih, kita harus mengganti panggilan untuk membuat objek baru dengan panggilan ke metode lokal, lalu membuat instance pengujian dari objek yang kita uji, dan mengganti metode pembuatan itu. Jadi kode programnya seperti ini:

class DataProcessor
{
protected function _newDataReader()
{
return new DataReader();
}

public function process()
{
$dr = $this->_newDataReader();
$json_data = $dr->readData();
$data = json_decode($json_data);
return $data;
}
}

Untuk menguji metode process() sekarang, kita perlu membuat beberapa perubahan pada kode pengujian kita. Kami memiliki dua opsi di sini: kami dapat mensubklasifikasikan DataProcessor menjadi TestableDataProcessor seperti di bawah ini:

class TestableDataProcessor extends DataProcessor
{
public $mock_dr;

protected function _newDataReader()
{
return $this->mock_dr;
}
}

class DataProcessorTest extends PHPUnit_Framework_TestCase
{
function testDataProcessorProcess_Subclass()
{
$mock_dr = $this->getMock("DataReader");
$mock_dr->expects($this->any())
->method("readData")
->will($this->returnValue('{"data": [4,5,6]}'));

$dp = new TestableDataProcessor();
$dp->mock_dr = $mock_dr;

$result = $dp->process();
$this->assertInstanceOf("stdClass", $result);
$this->assertCount(3, $result->data);
$this->assertEquals(4, $result->data[0]);
$this->assertEquals(5, $result->data[1]);
$this->assertEquals(6, $result->data[2]);
}
}

Pilihan lainnya adalah menggunakan fungsi tiruan PHPUnit untuk membuat tiruan objek yang diuji dan mengganti _newDataReader. Ini agak tidak biasa, tetapi kita dapat menggunakan akses ke kelas MockBuilder untuk mengonfigurasi objek tiruan yang diuji untuk menggantikan satu-satunya metode yang perlu kita timpa:

class DataProcessorTest extends PHPUnit_Framework_TestCase
{
function testDataProcessorProcess_Mock()
{
$mock_dr = $this->getMock("DataReader");
$mock_dr->expects($this->any())
->method("readData")
->will($this->returnValue('{"data": [4,5,6]}'));

$dp = $this->getMockBuilder("DataProcessor")
->setMethods(array("_newDataReader"))
->getMock();
$dp->expects($this->once())
->method("_newDataReader")
->will($this->returnValue($mock_dr));


$result = $dp->process();
$this->assertInstanceOf("stdClass", $result);
$this->assertCount(3, $result->data);
$this->assertEquals(4, $result->data[0]);
$this->assertEquals(5, $result->data[1]);
$this->assertEquals(6, $result->data[2]);
}
}

Verifikasi pemanggilan metode ke objek tiruan

Seperti yang kita temui sebelumnya, saat membuat metode tiruan, kita perlu menentukan berapa kali metode diharapkan dipanggil oleh kode yang diuji. Kita dapat menggunakan ini untuk memastikan bahwa kode yang diuji melakukan panggilan yang kita harapkan selama operasinya. Objek tiruan secara otomatis memeriksa status metode yang dipanggilnya sebagai bagian dari evaluasi pengujian, jadi kita bahkan tidak perlu menambahkan lebih banyak kode untuk mengaktifkan fungsi ini.

Kita dapat memainkannya dengan sangat cepat menggunakan kode dari contoh kita sebelumnya. Ganti $this->any() dengan $this->exactly(2) dan jalankan pengujian. Anda akan melihat bahwa itu gagal. Ganti dengan $this->once() dan itu akan dikirim lagi.

Sendiri, itu tidak terlalu menarik. Apa yang akan sangat berguna adalah dapat memvalidasi input apa pun yang diteruskan ke objek dummy. Mari ubah program utama kita untuk meneruskan URL ke JSON yang ingin kita unduh sebagai parameter:

class DataReader
{
public function readData($url)
{
$json = file_get_contents($url);
return $json;
}
}

class DataProcessor
{
protected function _newDataReader()
{
return new DataReader();
}

public function process()
{
$dr = $this->_newDataReader();
$json_data = $dr->readData("http://stainless-software.com/stm/json");
$data = json_decode($json_data);
return $data;
}
}

Memilih rute tiruan untuk menimpa objek kita di bawah metode pengujian _newDataReader lama berarti kita hanya perlu menambahkan satu baris kode untuk memverifikasi bahwa metode DataProcessor memang memanggil URL yang benar:

$mock_dr->expects($this->once())
->method("readData")
->with($this->equalTo("http://stainless-software.com/stm/json"))
->will($this->returnValue('{"data": [4,5,6]}'));

Konfirmasi beberapa panggilan

Setiap proyek pengujian yang cukup rumit akan mengharuskan Anda untuk membahas kasus di mana objek yang diuji memanggil metode yang sama pada objek yang berbeda dengan parameter yang berbeda setiap kali. Hilangkan metode ini untuk memberikan respons dummy untuk beberapa panggilan dan juga untuk memverifikasi bahwa parameter input untuk panggilan tersebut relatif sederhana.

Untuk menangani berbagai nilai pengembalian, cukup gunakan metode returnValueMap seperti yang dijelaskan dalam manual PHPUnit. Perhatikan bahwa peta yang valid adalah semua parameter input ke metode yang akan di-hash, diikuti dengan nilai kembalian sebagai input tambahan terakhir ke peta. Jadi sesuatu seperti ini:

$url1 = "http://stainless-software.com/stm/json";
$url2 = "http://stainless-software.com/stm/json2";
$mock_dr->expects($this->once())
->method("readData")
->will($this->returnValueMap(
array (
array($url1, '{"data": [1,2,3]}'),
array($url2, '{"data": [4,5,6]}')
)));

Untuk memverifikasi kumpulan parameter input yang berurutan, mari kita lihat contoh yang lebih lengkap:

class DataProcessor
{
protected function _newDataReader()
{
return new DataReader();
}

public function process()
{
$dr = $this->_newDataReader();
$json_data1 = $dr->readData("http://stainless-software.com/stm/json");
$data1 = json_decode($json_data1);
$json_data2 = $dr->readData("http://stainless-software.com/stm/json2");
$data2 = json_decode($json_data2);
return true;
}
}

Kunci untuk menguji dua metode serupa adalah dengan menggunakan pencocokan “at” untuk menentukan respons terhadap dua panggilan dan juga menambahkan ekspektasi ekstra untuk membuat tiruan jumlah panggilan yang tepat. Dengan menggunakan at($x), Anda memastikan bahwa metode dipanggil dengan argumen yang Anda harapkan dalam urutan yang Anda harapkan, tidak seperti returnValueMap, yang tidak memberlakukan batasan pemesanan.

class DataProcessorTester extends PHPUnit_Framework_TestCase
{
function testDataProcessorProcess_Mock()
{
$mock_dr = $this->getMock("DataReader");
$mock_dr->expects($this->exactly(2))
->method("readData");
$mock_dr->expects($this->at(0))
->method("readData")
->with($this->equalTo("http://stainless-software.com/stm/json"))
->will($this->returnValue('{"data": [1,2,3]}'));
$mock_dr->expects($this->at(1))
->method("readData")
->with($this->equalTo("http://stainless-software.com/stm/json2"))
->will($this->returnValue('{"data": [4,5,6]}'));

$dp = $this->getMockBuilder("DataProcessor")
->setMethods(array("_newDataReader"))
->getMock();
$dp->expects($this->once())
->method("_newDataReader")
->will($this->returnValue($mock_dr));
$result = $dp->process();
$this->assertTrue($result);
}
}

Kesimpulan

PHPUnit dapat digunakan untuk mengisolasi kode Anda yang sedang diuji dari bagian lain aplikasi Anda untuk mengujinya tanpa bergantung pada potongan kode lainnya. Ini berguna karena memecah masalah pengujian kode Anda menjadi potongan-potongan kecil yang terpisah. Pengujian unit untuk kode yang berinteraksi dengan potongan kode lainnya juga berguna untuk membantu mempertahankan konvensi antarmuka antara dua potongan kode.

Kita telah melihat di sini bagaimana mensimulasikan input dan memverifikasi output dari metode yang bergantung pada kode lain. Nantikan tips tentang bagaimana dan kapan menggunakan PHPUnit agar lebih efektif, cara menggunakan PHPUnit saat bekerja dengan kerangka kerja, dan cara menggunakan PHPUnit untuk menguji metode statis.

Referensi

[1] http://phpunit.de/manual/current/en/test-doubles.html

Tentang Penulis

Kendrick Curtis adalah pengembang web dengan pengalaman sepuluh tahun. Dia adalah salah satu pendiri Stainless Software, sebuah perusahaan desain, pengembangan, pengujian, dan produksi konten independen. Informasi lebih lanjut di http://www.stainless-software.com/

[ad_2]

Source link

Leave a Reply

Your email address will not be published. Required fields are marked *