Hai, Habr.

Ya, ini adalah pos lain tentang topik pengujian. Tampaknya di sini sudah mungkin untuk membahas? Semua yang membutuhkannya - mereka menulis tes, siapa yang tidak membutuhkannya - mereka tidak menulis, semua orang senang! Faktanya adalah bahwa sebagian besar posting tentang pengujian unit memiliki ... bagaimana menyinggung siapa pun ... contoh bodoh! Tidak, sungguh! Hari ini saya akan mencoba memperbaikinya. Saya minta kucing.
Maka, pencarian cepat pada topik pengujian hanya menemukan banyak artikel, yang dalam jumlah besar dibagi menjadi dua kategori:
1) Kebahagiaan seorang copywriter. Pertama kita melihat pengantar yang panjang, kemudian sejarah pengujian unit di Rusia Kuno, lalu sepuluh peretasan dengan tes, dan pada akhirnya, sebuah contoh. Dengan pengujian kode seperti ini:
<?php class Calculator { public function plus($a, $b) { return $a + $b; } }
Dan saya tidak bercanda sekarang. Saya benar-benar melihat artikel dengan "kalkulator" sebagai panduan belajar. Ya, ya, saya mengerti bahwa untuk memulainya perlu menyederhanakan segalanya, abstraksi, bolak-balik ... Tapi di sinilah semuanya berakhir! Dan kemudian menyelesaikan burung hantu, seperti yang mereka katakan
2) Contoh-contoh yang terlalu canggih. Dan mari kita menulis tes, dan menjejalkannya ke Gitlab CI, dan kemudian kita akan memperbaikinya secara otomatis jika tes lulus, dan kita akan menerapkan PHP Infection ke tes, tetapi kami akan menghubungkan semuanya ke Hudson. Dan seterusnya dalam gaya itu. Tampaknya bermanfaat, tetapi tampaknya itu sama sekali tidak apa yang Anda cari. Tetapi Anda hanya ingin sedikit meningkatkan stabilitas proyek Anda. Dan semua kontinuitas ini - yah, tidak sekaligus.
Akibatnya, orang ragu, "Tetapi apakah saya membutuhkannya." Saya, pada gilirannya, ingin mencoba menjelaskan dengan lebih jelas tentang pengujian. Dan segera lakukan reservasi - saya seorang pengembang, saya bukan penguji. Saya yakin bahwa saya sendiri tidak tahu banyak, dan kata pertama saya dalam hidup saya bukanlah kata "mok". Saya bahkan belum pernah bekerja di TDD! Tapi saya tahu pasti bahwa bahkan tingkat keterampilan saya saat ini telah memungkinkan saya untuk menutup beberapa proyek dengan tes, dan tes ini sudah menangkap selusin bug. Dan jika itu membantu saya, maka itu bisa membantu orang lain. Beberapa bug yang tertangkap akan sulit ditangkap secara manual.
Untuk mulai dengan, program pendidikan singkat dalam format tanya jawab:
T: Apakah saya harus menggunakan semacam kerangka kerja? Bagaimana jika saya memiliki Yii? Bagaimana jika Kohana? Bagaimana jika% one_more_framework_name%?
A: Tidak, PHPUnit adalah kerangka kerja pengujian independen, Anda bahkan dapat mengacaukannya pada kode lawas pada kerangka kerja buatan sendiri.
T: Dan sekarang saya dengan cepat menelusuri situs dengan tangan saya, dan itu normal. Mengapa saya membutuhkannya?
A: "Jalankan" dari beberapa lusin tes berlangsung beberapa detik. Pengujian otomatis selalu lebih cepat daripada pengujian manual, dan dengan pengujian berkualitas tinggi juga lebih andal, karena mencakup semua skenario.
T: Saya memiliki kode lawas dengan fungsi 2000 baris. Bisakah saya menguji ini?
A: Ya dan tidak. Secara teori, ya, kode apa pun dapat ditutupi dengan tes. Dalam praktiknya, kode harus ditulis dengan dasar untuk pengujian di masa depan. Fungsi 2000 baris akan memiliki terlalu banyak dependensi, cabang, kasus perbatasan. Mungkin pada akhirnya akan mencakup semuanya, tetapi kemungkinan besar itu akan memakan waktu lama bagi Anda. Semakin baik kode, semakin mudah untuk mengujinya. Tanggung jawab tunggal yang lebih baik dihormati, semakin mudah tes akan. Untuk menguji proyek lama paling sering, Anda harus terlebih dahulu refactor mereka dengan dingin.

T: Saya memiliki metode (fungsi) yang sangat sederhana, apa yang harus diuji? Semuanya bisa diandalkan di sana, tidak ada ruang untuk kesalahan!
A: Harus dipahami bahwa Anda tidak menguji implementasi fungsi yang benar (jika Anda tidak memiliki TDD), Anda cukup "memperbaiki" kondisi kerjanya saat ini. Di masa depan, ketika Anda perlu mengubahnya, Anda dapat dengan cepat menentukan apakah Anda melanggar perilakunya menggunakan tes. Contoh: ada fungsi yang memvalidasi email. Dia membuatnya menjadi biasa.
function isValid($email) { $regex = "very_complex_regex_here"; if (is_array($email)) { $result = true; foreach ($email as $item) { if (preg_match($regex, $item) === 0) { $result = false; } } } else { $result = preg_match($regex, $emai) ==! 0; } return $result; }
Semua kode Anda mengharapkan bahwa jika Anda mengirim email yang valid ke fungsi ini, itu akan mengembalikan true. Array email yang valid juga benar. Array dengan setidaknya satu alamat email tidak valid salah. Nah dan seterusnya, kodenya jelas. Tetapi hari itu tiba, dan Anda memutuskan untuk mengganti musim reguler yang mengerikan dengan API eksternal. Tetapi bagaimana menjamin bahwa fungsi yang ditulis ulang tidak mengubah prinsip operasi? Tiba-tiba tidak menangani array dengan baik? Atau akankah kembali bukan boolean? Dan tes dapat menjaga ini di bawah kendali. Tes yang ditulis dengan baik akan segera menunjukkan perilaku fungsi selain yang diharapkan.
T: Kapan saya akan mulai melihat beberapa perasaan dari tes?
A: Pertama, segera setelah Anda membahas bagian penting dari kode. Semakin dekat cakupan ke 100%, semakin dapat diandalkan pengujian. Kedua, segera setelah Anda harus membuat perubahan global, atau perubahan pada bagian kompleks kode. Tes dapat menangkap masalah yang dapat dengan mudah terjawab secara manual (kasus batas). Ketiga, saat menulis tes sendiri! Seringkali ada situasi ketika menulis tes mengungkapkan kesalahan kode yang tidak terlihat pada pandangan pertama.
T: Ya, saya punya situs web di laravel. Situs ini bukan fungsi, situs ini adalah gunung kode yang menyebalkan. Bagaimana cara menguji di sini?
A: Inilah yang akan dibahas nanti. Singkatnya: kami menguji secara terpisah metode pengontrol, secara terpisah middleware, secara terpisah layanan, dll.
Salah satu ide pengujian Unit adalah untuk mengisolasi bagian kode yang diuji. Semakin sedikit kode yang Anda uji dengan satu tes, semakin baik. Mari kita lihat contoh sedekat mungkin dengan kehidupan nyata:
<?php class Controller { public function __construct($userService, $emailService) { $this->userService = $userService; $this->emailService = $emailService; } public function login($request) { if (empty($request->login) || empty($request->password)) { return "Auth error"; } $password = $this->userService->getPasswordFor($request->login); if (empty($password)) { return "Auth error - no password"; } if ($password !== $request->password) { return "Incorrect password"; } $this->emailService->sendEmail($request->login); return "Success"; } }
Ini adalah metode yang sangat khas untuk masuk ke sistem pada proyek-proyek kecil. Yang kami harapkan adalah pesan kesalahan yang benar, dan email yang dikirim jika berhasil masuk. Bagaimana cara menguji metode ini? Pertama, Anda perlu mengidentifikasi dependensi eksternal. Dalam kasus kami, ada dua di antaranya - $ userService dan $ emailService. Mereka melewati konstruktor kelas, yang sangat memudahkan tugas kita. Tapi, seperti yang disebutkan sebelumnya, semakin sedikit kode yang kami uji dalam satu pass, semakin baik.
Persaingan, penggantian objek disebut mokanem (dari bahasa Inggris. Objek tiruan, secara harfiah: "objek-parodi"). Tidak ada yang mengganggu untuk menulis objek seperti itu secara manual, tetapi semuanya telah ditemukan sebelum kita, sehingga perpustakaan yang luar biasa seperti
Mockery datang untuk menyelamatkan. Mari kita membuat mokas untuk layanan.
$userService = Mockery::mock('user_service'); $emailService = Mockery::mock('email_service');
Sekarang buat objek $ request. Untuk mulai dengan, kami akan menguji logika memeriksa bidang login dan kata sandi. Kami ingin memastikan bahwa jika tidak ada, metode kami akan menangani kasus ini dengan benar dan mengembalikan Pesan (!) Yang diinginkan.
function testEmptyLogin() { $userService = Mockery::mock('user_service'); $emailService = Mockery::mock('email_service'); $controller = new Controller($userService, $emailService); $request = (object) []; $result = $controller->login($request); }
Tidak ada yang rumit, bukan? Kami membuat stubs untuk parameter kelas yang diperlukan, membuat instance dari kelas yang diinginkan, dan "menarik" metode yang diinginkan, dengan sengaja melewatkan permintaan yang salah. Mendapat jawaban. Tapi bagaimana cara memeriksanya sekarang? Ini adalah bagian paling penting dari tes ini - apa yang disebut pernyataan. PHPUnit memiliki lusinan
pernyataan siap pakai. Cukup gunakan salah satunya.
function testEmptyLogin() { $userService = Mockery::mock('user_service'); $emailService = Mockery::mock('email_service'); $controller = new Controller($userService, $emailService); $request = (object) []; $result = $controller->login($request);
Tes ini menjamin yang berikut - jika argumen login tiba di objek metode yang tidak memiliki bidang login atau kata sandi, maka metode akan mengembalikan string "Kesalahan auth". Itu, secara umum, itu saja. Sangat sederhana - tetapi sangat berguna, karena sekarang kita dapat mengedit metode login tanpa takut merusak sesuatu. Frontend kita dapat memastikan bahwa jika sesuatu terjadi - dia akan mendapatkan kesalahan seperti itu. Dan jika seseorang merusak perilaku ini (misalnya, memutuskan untuk mengubah teks kesalahan), maka tes akan segera memberi sinyal ini! Kami menambahkan sisa cek untuk mencakup sebanyak mungkin skenario.
function testEmptyPassword() { $userService = Mockery::mock('user_service');
Perhatikan metode shouldReceive dan andReturn? Mereka memungkinkan kita untuk membuat metode dalam bertopik yang hanya mengembalikan apa yang kita butuhkan. Perlu menguji kesalahan kata sandi yang salah? Kami menulis stub $ userService yang selalu mengembalikan kata sandi yang salah. Dan itu dia.
Dan bagaimana dengan dependensi, Anda bertanya. Kami kemudian “menenggelamkan” mereka, dan bagaimana jika mereka pecah? Tapi inilah tepatnya cakupan kode maksimum dengan tes. Kami tidak akan memeriksa operasi layanan ini dalam konteks login - kami akan menguji login dengan harapan operasi layanan yang benar. Dan kemudian kami menulis tes terisolasi yang sama untuk layanan ini. Dan kemudian menguji ketergantungan mereka. Dan sebagainya. Akibatnya, setiap tes individu
hanya menjamin operasi yang benar dari sepotong kecil kode, asalkan semua dependensinya bekerja dengan benar. Dan karena semua dependensi juga dicakup oleh tes, operasi yang benar juga dijamin. Akibatnya, setiap perubahan pada sistem yang merusak logika pekerjaan bahkan bagian terkecil dari kode akan segera muncul dalam tes tertentu. Cara khusus menjalankan uji coba - saya tidak akan memberi tahu, dokumentasi di PHPUnit cukup baik. Dan di Laravel, misalnya, cukup menjalankan vendor / bin / phpunit dari root proyek untuk melihat pesan seperti ini

- Semua tes berhasil. Atau sesuatu seperti ini

Satu dari tujuh pernyataan gagal.
"Ini, tentu saja, keren, tapi apa yang tidak bisa kukerjakan?" Anda bertanya. Dan mari kita bayangkan kode berikut untuk ini
<?php function getInfo($infoApi, $userName) { $response = $infoApi->getInfo($userName); if ($response->status === "API Error") { return null; } return $response->result; }
Kami melihat model yang disederhanakan untuk bekerja dengan API eksternal. Fungsi ini menggunakan beberapa kelas untuk bekerja dengan API, dan jika terjadi kesalahan, mengembalikan nol. Jika, ketika menggunakan fungsi ini, kita mendapatkan nol, kita harus "meningkatkan kepanikan" (mengirim pesan ke slack, atau mengirim email ke pengembang, atau melemparkan kesalahan ke dalam kibana. Ya, banyak pilihan). Segalanya tampak sederhana, bukan? Tetapi bayangkan bahwa setelah beberapa waktu pengembang lain memutuskan untuk "memperbaiki" fungsi ini. Dia memutuskan bahwa mengembalikan nol adalah abad terakhir, dan dia harus melemparkan pengecualian.
function getInfo($infoApi, $userName): string { $response = $infoApi->getInfo($userName); if ($response->status === "API Error") { throw new ApiException($response); } return $response->result; }
Dan dia bahkan menulis ulang semua bagian kode tempat fungsi ini dipanggil! Semua kecuali satu. Dia merindukannya. Mengalihkan perhatian, lelah, salah - tetapi Anda tidak pernah tahu. Faktanya adalah bahwa sepotong kode masih menunggu perilaku fungsi lama. Dan PHP bukan Java untuk kami - kami tidak akan mendapatkan kesalahan kompilasi dengan alasan bahwa fungsi throwable tidak dibungkus dengan try-catch. Akibatnya, dalam salah satu dari 100 skenario untuk menggunakan situs ini, jika terjadi kerusakan API, kami tidak akan menerima pesan dari sistem. Selain itu, dengan pengujian manual, kemungkinan besar kami tidak akan menangkap versi acara ini. API bersifat eksternal, tidak tergantung pada kami, itu berfungsi dengan baik - dan kemungkinan besar kami tidak akan mendapatkannya jika terjadi kegagalan API, dan penanganan pengecualian yang salah. Tetapi jika kita memiliki tes, mereka akan menangkap kasus ini dengan sangat baik, karena kelas ExternalApi “teredam” dalam sejumlah tes, dan ini mengemulasi perilaku normal dan crash. Dan tes selanjutnya akan jatuh
function testApiFail() { $api = Mockery::mock('api'); $api->shouldReceive('getInfo')->andReturn((object) [ 'status' => 'API Error' ]); $result = getInfo($api, 'name'); $this->assertNull($result); }
Informasi ini sebenarnya cukup. Jika Anda tidak memiliki mie Legacy, setelah 20-30 menit Anda dapat menulis tes pertama Anda. Dan beberapa minggu kemudian - untuk mempelajari sesuatu yang baru, keren, kembali ke komentar di bawah posting ini, dan tulis penulis mana yang govnokoder tidak tahu tentang% framework_name%, dan menulis tes buruk, tetapi Anda perlu melakukan% this_way%. Dan saya akan sangat senang dalam hal ini. Ini akan berarti bahwa tujuan saya telah tercapai: orang lain menemukan pengujian untuk dirinya sendiri, dan sedikit meningkatkan tingkat profesionalisme secara umum di bidang kita!
Kritik yang masuk akal disambut.