Bagaimana cara mengevaluasi kualitas tes? Banyak yang mengandalkan metrik paling populer yang dikenal semua orang - cakupan kode. Tapi ini kuantitatif, bukan metrik kualitatif. Ini menunjukkan seberapa banyak kode Anda dicakup oleh tes, tetapi tidak seberapa baik tes ini ditulis.
Salah satu cara untuk mencari tahu ini adalah dengan pengujian mutasi. Alat ini, membuat perubahan kecil pada kode sumber dan menjalankan kembali pengujian setelah itu, memungkinkan Anda untuk mengidentifikasi tes yang tidak berguna dan cakupan berkualitas rendah.
Di
Badoo PHP Meetup pada bulan Maret, saya berbicara tentang bagaimana mengatur pengujian mutasi untuk kode PHP dan masalah apa yang mungkin Anda temui. Video tersedia di
sini , dan untuk versi teks, selamat datang di cat.

Apa itu pengujian mutasi
Untuk menjelaskan apa yang saya maksud, saya akan menunjukkan kepada Anda beberapa contoh. Mereka sederhana, dilebih-lebihkan di beberapa tempat dan mungkin tampak jelas (walaupun contoh nyata biasanya cukup kompleks dan tidak dapat dilihat dengan mata mereka).
Pertimbangkan situasinya: kita memiliki fungsi dasar yang mengaku sebagai orang dewasa, dan ada tes yang mengujinya. Tes ini memiliki dataProvider, yaitu tes dua kasus: usia 17 tahun dan usia 19 tahun. Saya pikir jelas bagi banyak dari Anda bahwa Dewasa memiliki cakupan 100%. Satu-satunya garis. Itu dilakukan dengan tes. Semuanya luar biasa.

Tetapi pemeriksaan lebih dekat mengungkapkan bahwa penyedia kami ditulis dengan buruk dan tidak menguji kondisi batas: usia 18 tahun sebagai syarat batas tidak diuji. Anda dapat mengganti tanda> dengan> =, dan tes tidak akan menangkap perubahan seperti itu.
Contoh lain, sedikit lebih rumit. Ada fungsi yang membangun beberapa objek sederhana yang berisi setter dan getter. Kami memiliki tiga bidang yang kami atur, dan ada tes yang memeriksa bahwa fungsi buildPromoBlock benar-benar mengumpulkan objek yang kami harapkan.

Jika Anda melihat lebih dekat, kami juga memiliki setSomething, yang menetapkan beberapa properti menjadi true. Tetapi dalam tes kami tidak memiliki pernyataan seperti itu. Artinya, kita dapat menghapus baris ini dari buildPromoBlock - dan pengujian kami tidak akan menangkap perubahan ini. Pada saat yang sama, kami memiliki cakupan 100% dalam fungsi buildPromoBlock, karena ketiga jalur dieksekusi selama pengujian.
Dua contoh ini menuntun kita pada pengujian mutasi.
Sebelum membongkar algoritma, saya akan memberikan definisi singkat. Pengujian mutasi adalah mekanisme yang memungkinkan kita, membuat perubahan kecil pada kode, untuk meniru tindakan Pinocchio jahat atau Vasya junior, yang datang dan mulai memecahkannya dengan sengaja, ganti tanda> dengan <, = oleh! =, Dan seterusnya. Untuk setiap perubahan yang kami buat untuk tujuan yang baik, kami menjalankan tes yang harus mencakup baris yang diubah.
Jika tes tidak menunjukkan kepada kami apa pun, jika tidak jatuh, maka itu mungkin tidak cukup efektif. Mereka tidak menguji kasus batas, tidak mengandung pernyataan: mungkin perlu diperbaiki. Jika tes jatuh, maka itu keren. Mereka benar-benar melindungi dari perubahan seperti itu. Karenanya, kode kami lebih sulit untuk dipatahkan.
Sekarang mari kita menganalisis algoritme. Cukup sederhana. Hal pertama yang kami lakukan untuk melakukan pengujian mutasi adalah mengambil kode sumber. Selanjutnya, kita mendapatkan cakupan kode untuk mengetahui tes mana yang harus dijalankan untuk string mana. Setelah itu, kita membahas kode sumber dan menghasilkan apa yang disebut mutan.
Mutan adalah perubahan kode tunggal. Yaitu, kita mengambil fungsi tertentu di mana ada> perbandingan masuk, jika, kita mengubah tanda ini ke> = - dan kita mendapatkan mutan. Setelah itu, kami menjalankan tes. Berikut ini contoh mutasi (kami ganti> dengan> =):

Dalam hal ini, mutasi tidak dilakukan secara acak, tetapi sesuai aturan tertentu. Respons pengujian mutasi adalah idempoten. Tidak peduli berapa kali kita menjalankan pengujian mutasi pada kode yang sama, itu menghasilkan hasil yang sama.
Hal terakhir yang kami lakukan adalah menjalankan tes yang mencakup garis yang dimutasi. Dapatkan dari jangkauan. Ada alat yang tidak optimal yang menggerakkan semua tes. Tetapi alat yang baik hanya akan mengusir mereka yang dibutuhkan.
Setelah itu, kami mengevaluasi hasilnya. Tes jatuh - maka semuanya baik-baik saja. Jika mereka tidak jatuh, maka mereka tidak terlalu efektif.
Metrik
Metrik apa yang diberikan pengujian mutasi kepada kita? Ia menambahkan tiga lagi ke cakupan kode, yang akan kita bicarakan sekarang.
Tapi pertama-tama, mari kita menganalisis terminologinya.

Ada konsep mutan yang terbunuh: ini adalah mutan yang uji kami “paku” (yaitu, mereka menangkapnya).

Ada konsep melarikan diri mutan (surviving mutants). Ini adalah mutan yang berhasil menghindari hukuman (yaitu, tes tidak menangkap mereka).

Dan ada konsep yang dibahas mutan - mutan yang tercakup oleh tes, dan mutan yang ditemukan berlawanan dengannya, yang tidak tercakup oleh tes sama sekali (mis. Kita memiliki kode, memiliki logika bisnis, kita dapat mengubahnya, tetapi tidak satu tes pun tidak memeriksa perubahan).
Indikator utama yang diberikan pengujian mutasi kepada kita adalah MSI (indikator skor mutasi), rasio jumlah mutan yang terbunuh dengan jumlah totalnya.
Indikator kedua adalah cakupan kode mutasi. Itu hanya kualitatif, bukan kuantitatif, karena itu menunjukkan seberapa banyak logika bisnis yang dapat Anda hancurkan dan lakukan secara teratur, pengujian kami tertangkap.
Dan metrik terakhir dibahas MSI, yaitu MSI yang lebih lunak. Dalam hal ini, kami menghitung MSI hanya untuk mutan yang dicakup oleh tes.
Masalah Pengujian Mutasi
Mengapa kurang dari setengah programmer mendengar tentang alat ini? Mengapa tidak digunakan di mana-mana?
Kecepatan rendah
Masalah pertama (salah satu yang utama) adalah kecepatan pengujian mutasi. Dalam kode, jika kita memiliki lusinan operator mutasi, bahkan untuk kelas paling sederhana, kita dapat menghasilkan ratusan mutasi. Untuk setiap mutasi, Anda harus menjalankan tes. Jika kita memiliki, katakanlah, 5.000 tes unit yang berjalan selama sepuluh menit, pengujian mutasi dapat berlangsung berjam-jam.
Apa yang bisa dilakukan untuk menaikkan level ini? Jalankan tes secara paralel, dalam beberapa utas. Lemparkan aliran ke beberapa mobil. Itu bekerja.
Cara kedua adalah menjalankan incremental. Tidak perlu menghitung indikator mutasi untuk seluruh cabang setiap kali - Anda dapat mengambil diff cabang. Jika Anda menggunakan fitur brunch, akan mudah bagi Anda untuk melakukan ini: jalankan tes hanya pada file-file yang telah berubah, dan lihat apa yang terjadi di wisaya, bandingkan, analisis.
Hal berikutnya yang dapat Anda lakukan adalah penyetelan mutasi. Karena operator mutasi dapat diubah, Anda dapat menetapkan aturan tertentu yang digunakannya, lalu Anda dapat menghentikan beberapa mutasi jika mereka secara sadar menyebabkan masalah.
Poin penting: pengujian mutasional hanya cocok untuk pengujian unit. Terlepas dari kenyataan bahwa itu dapat dijalankan untuk tes integrasi, ini jelas merupakan ide yang gagal, karena tes integrasi (seperti ujung ke ujung) berjalan jauh lebih lambat dan mempengaruhi kode lebih banyak. Anda tidak akan pernah menunggu hasilnya. Pada prinsipnya, mekanisme ini diciptakan dan dikembangkan secara eksklusif untuk pengujian unit.
Mutan tanpa akhir
Masalah kedua yang dapat muncul dengan tes mutasi adalah apa yang disebut mutan tanpa akhir. Misalnya, ada kode sederhana, sederhana untuk loop:

Jika Anda mengganti i ++ dengan i--, maka siklus akan berubah menjadi tak terbatas. Kode Anda akan menempel untuk waktu yang lama. Dan pengujian mutasional cukup sering menghasilkan mutasi seperti itu.
Hal pertama yang dapat Anda lakukan adalah menyetel mutasi. Jelas, mengubah i ++ ke i-- dalam for for adalah ide yang sangat buruk: dalam 99% kasus kita akan berakhir dengan infinite loop. Karena itu, kami dilarang melakukan ini di alat kami.
Hal kedua dan terpenting yang melindungi Anda dari masalah seperti itu adalah batas waktu untuk lari. Misalnya, PHPUnit yang sama memiliki kemampuan untuk menyelesaikan tes batas waktu terlepas dari di mana ia macet. PHPUnit melalui PCNTL menutup panggilan balik dan menghitung waktu itu sendiri. Jika pengujian gagal untuk periode tertentu, itu hanya dipaku dan kasus seperti itu dianggap sebagai mutan yang terbunuh, karena kode yang menghasilkan mutasi benar-benar diperiksa oleh tes, yang benar-benar menangkap masalah, yang menunjukkan bahwa kode tersebut menjadi tidak beroperasi.
Mutan identik
Masalah ini ada dalam teori pengujian mutasi. Dalam praktiknya, mereka tidak terlalu sering menemukannya, tetapi Anda perlu mengetahuinya.
Pertimbangkan contoh klasik yang menggambarkannya. Kami memiliki perkalian variabel A dengan -1 dan pembagian A dengan -1. Dalam kasus umum, operasi ini menghasilkan hasil yang sama. Kami mengubah tanda A. Oleh karena itu, kami memiliki mutasi yang memungkinkan dua tanda untuk berubah di antara mereka sendiri. Logika program dengan mutasi semacam itu tidak dilanggar. Tes dan seharusnya tidak menangkapnya, tidak boleh jatuh. Karena mutan yang identik seperti itu, beberapa kesulitan muncul.
Tidak ada solusi universal - semua orang menyelesaikan masalah ini dengan caranya sendiri. Mungkin semacam sistem pendaftaran mutan akan membantu. Kami di Badoo sedang memikirkan sesuatu yang serupa sekarang, kami akan meniru mereka.
Ini adalah teori. Bagaimana dengan PHP?
Ada dua alat yang terkenal untuk pengujian mutasi: Humbug dan Infeksi. Ketika saya sedang mempersiapkan artikel, saya ingin berbicara tentang mana yang lebih baik dan sampai pada kesimpulan bahwa ini adalah Infeksi.
Tetapi ketika saya pergi ke halaman Humbug, saya melihat yang berikut di sana: Humbug menyatakan dirinya usang dalam mendukung Infeksi. Karena itu, bagian dari artikel saya ternyata tidak ada artinya. Jadi Infeksi adalah alat yang sangat bagus. Saya harus mengucapkan terima kasih kepada
borNfree dari Minsk, yang membuatnya. Dia benar-benar bekerja dengan keren. Anda dapat mengambilnya langsung dari kotak, memasukkannya ke komposer dan memulainya.
Kami benar-benar menyukai Infeksi. Kami ingin menggunakannya. Tetapi mereka tidak bisa karena dua alasan. Infeksi memerlukan cakupan kode untuk menjalankan tes untuk mutan dengan benar dan tepat. Di sini kita punya dua cara. Kami dapat menghitungnya secara langsung dalam runtime (tetapi kami memiliki 100.000 unit tes). Atau kita dapat menghitungnya untuk master saat ini (tetapi membangun di cloud kami dari sepuluh mesin yang sangat kuat dalam beberapa utas membutuhkan waktu satu setengah jam). Jika kita melakukan ini pada setiap menjalankan mutasi, alat mungkin tidak akan berfungsi.
Ada opsi untuk memberi makan yang sudah jadi, tetapi dalam format PHPUnit, ini adalah banyak file XML. Selain fakta bahwa mereka mengandung informasi yang berharga, mereka menyeret banyak struktur, beberapa tanda kurung dan hal-hal lainnya. Saya pikir secara umum, cakupan kode kami akan mencapai 30 GB, dan kami harus menyeretnya ke semua mesin cloud, terus-menerus membaca dari disk. Secara umum, idenya begitu-begitu.
Masalah kedua bahkan lebih signifikan. Kami memiliki perpustakaan
SoftMocks yang luar
biasa . Ini memungkinkan kita untuk berurusan dengan kode lama, yang sulit untuk diuji, dan berhasil menulis tes untuk itu. Kami secara aktif menggunakannya dan tidak akan menolaknya dalam waktu dekat, meskipun kami sedang menulis kode baru sehingga kami tidak memerlukan SoftMock. Jadi, perpustakaan ini tidak kompatibel dengan Infeksi, karena mereka menggunakan pendekatan yang hampir sama untuk mengubah perubahan.
Bagaimana cara kerja SoftMock? Mereka mencegat inklusi file dan menggantinya dengan yang dimodifikasi, yaitu, alih-alih mengeksekusi kelas A, SoftMock membuat kelas A di tempat yang berbeda dan menghubungkan yang lain, bukan yang asli. Infeksi bekerja dengan cara yang persis sama, hanya saja ia berfungsi melalui
stream_wrapper_register () , yang melakukan hal yang sama, tetapi pada level sistem. Akibatnya, SoftMocks atau Infection dapat bekerja untuk kita. Karena SoftMock diperlukan untuk pengujian kami, sangat sulit untuk membuat kedua alat ini menjadi teman. Ini mungkin mungkin, tetapi dalam kasus ini kita masuk ke Infeksi sehingga makna dari perubahan seperti itu hilang begitu saja.
Mengatasi kesulitan, kami menulis instrumen kecil kami. Kami meminjam operator mutasi dari Infection (mereka ditulis dengan keren dan sangat mudah digunakan). Alih-alih memulai mutasi melalui stream_wrapper_register (), kami menjalankannya melalui SoftMocks, yaitu, kami menggunakan alat kami dari kotak. Toolza kami berteman dengan layanan cakupan kode internal kami. Yaitu, berdasarkan permintaan dapat menerima cakupan untuk file atau untuk baris tanpa menjalankan semua tes, yang terjadi sangat cepat. Namun, itu sederhana. Jika Infection memiliki banyak alat dan fitur (misalnya, diluncurkan di beberapa utas), maka Infeksi kami tidak. Tetapi kami menggunakan infrastruktur internal kami untuk mengimbangi kekurangan ini. Misalnya, kami menjalankan uji coba yang sama di beberapa utas melalui cloud kami.
Bagaimana kita menggunakan ini?
Yang pertama adalah menjalankan manual. Ini adalah hal pertama yang harus dilakukan. Semua tes yang Anda tulis diverifikasi secara manual oleh pengujian mutasi. Itu terlihat seperti ini:

Saya menjalankan tes mutasi untuk beberapa file. Mendapat hasil: 16 mutan. Dari jumlah tersebut, 15 terbunuh oleh tes, dan satu jatuh dengan kesalahan. Saya tidak mengatakan bahwa mutasi dapat menyebabkan kematian. Kami dapat dengan mudah mengubah sesuatu: membuat jenis pengembalian tidak valid, atau sesuatu yang lain. Ini mungkin, dianggap mutan yang terbunuh, karena pengujian kami akan mulai turun.
Namun demikian, Infeksi membedakan mutan-mutan seperti itu dalam kategori terpisah dengan alasan bahwa kadang-kadang layak untuk memberikan perhatian khusus pada kesalahan. Kebetulan terjadi sesuatu yang aneh - dan mutan tidak dianggap benar dibunuh.
Hal kedua yang kami gunakan adalah laporan tentang master. Sekali sehari, pada malam hari, ketika infrastruktur pembangunan kami menganggur, kami menghasilkan laporan cakupan kode. Setelah itu, kami membuat laporan pengujian mutasi yang sama. Ini terlihat seperti ini:

Jika Anda pernah melihat laporan tentang cakupan kode PHPUnit, Anda mungkin memperhatikan bahwa antarmuka mirip, karena kami membuat alat kami dengan analogi. Dia hanya menghitung semua indikator kunci untuk file tertentu dalam direktori. Kami juga menetapkan tujuan tertentu (pada kenyataannya, kami mengambilnya dari langit-langit dan belum mematuhinya, karena kami belum memutuskan tujuan mana yang harus dipandu oleh masing-masing metrik, tetapi mereka ada sehingga mudah untuk membuat laporan di masa mendatang).
Dan hal terakhir, yang paling penting, yang merupakan konsekuensi dari dua lainnya. Programmer adalah orang yang malas. Saya malas: Saya suka semuanya bekerja dan saya tidak harus membuat gerakan ekstra. Kami membuatnya sehingga ketika pengembang mendorong cabangnya sendiri, indikator cabangnya dan brunch master secara otomatis dihitung secara bertahap.

Sebagai contoh, saya menjalankan dua file dan mendapatkan hasil ini. Dalam master saya memiliki 548 mutan, 400 terbunuh. Menurut file lain - 147 melawan 63. Di cabang saya, jumlah mutan dalam kedua kasus meningkat. Tapi di file pertama, mutan itu dipaku, dan di file kedua, dia melarikan diri. Secara alami, indikator MSI turun. Hal semacam itu memungkinkan bahkan orang yang tidak ingin membuang waktu untuk menjalankan pengujian mutasi dengan tangan mereka, melihat apa yang telah mereka lakukan lebih buruk, dan memperhatikannya (persis sama dengan cara yang dilakukan oleh pengulas dalam proses review kode).
Hasil
Masih sulit untuk memberikan angka: kami tidak memiliki indikator, sekarang sudah muncul, tetapi tidak ada yang bisa dibandingkan.
Saya dapat mengatakan bahwa pengujian mutasional memberikan efek psikologis. Jika Anda mulai menjalankan tes Anda melalui pengujian mutasi, Anda tanpa sadar mulai menulis tes yang lebih baik, dan menulis tes kualitas pasti mengarah pada perubahan dalam cara Anda menulis kode - Anda mulai berpikir bahwa Anda perlu mencakup semua kasus yang dapat Anda hancurkan, Anda memulainya struktur yang lebih baik, membuatnya lebih dapat diuji.
Ini adalah opini yang eksklusif subjektif. Tetapi beberapa kolega saya memberikan umpan balik yang kira-kira sama: ketika mereka mulai terus menggunakan pengujian mutasi dalam pekerjaan mereka, mereka mulai menulis tes dengan lebih baik, dan banyak yang mengatakan bahwa mereka mulai menulis kode dengan lebih baik.
Kesimpulan
Cakupan kode adalah metrik penting yang perlu dipantau. Tetapi indikator ini tidak menjamin apa-apa: itu tidak berarti Anda aman.
Pengujian mutasi dapat membantu menjadikan pengujian unit Anda lebih baik, dan melacak cakupan kode masuk akal. Sudah ada alat untuk PHP, jadi jika Anda memiliki proyek kecil tanpa masalah, maka ambil dan coba hari ini.
Mulai setidaknya dengan menjalankan tes mutasi secara manual. Ambil langkah sederhana ini dan lihat apa yang memberi Anda. Saya yakin Anda akan menyukainya.