Tes menulis harus menginspirasi kepercayaan diri dalam pengoperasian kode yang benar. Seringkali kita beroperasi pada tingkat cakupan kode, dan ketika kita mencapai 100%, kita dapat mengatakan bahwa solusinya benar. Apakah Anda yakin tentang ini? Mungkin ada alat yang akan memberikan umpan balik yang lebih akurat?
Pengujian Mutasi
Istilah ini menjelaskan situasi di mana kami memodifikasi potongan kode kecil dan melihat bagaimana ini memengaruhi tes. Jika, setelah perubahan, tes dilakukan dengan benar, ini menunjukkan bahwa untuk potongan kode tes tidak cukup. Tentu saja, itu semua tergantung pada apa yang sebenarnya kita ubah, karena kita tidak perlu menguji semua perubahan terkecil, misalnya, indentasi atau nama variabel, karena setelah itu pengujian juga harus lengkap dengan benar. Oleh karena itu, dalam pengujian mutasi, kami menggunakan apa yang disebut mutator (metode pengubah), yang mengganti satu bagian kode dengan yang lain, tetapi dengan cara yang masuk akal. Kami akan membicarakan hal ini secara lebih rinci di bawah ini. Kadang-kadang kita melakukan tes semacam itu sendiri, memeriksa apakah tes tersebut pecah jika kita mengubah sesuatu dalam kode. Jika kita refactored "setengah sistem" dan tes masih hijau, maka kita dapat langsung mengatakan bahwa itu buruk. Dan jika seseorang melakukan ini dan tesnya baik, maka selamat!
Kerangka Kerja Menular
Hari ini di PHP, kerangka pengujian mutasi yang paling populer adalah
Infection . Ini mendukung PHPUnit dan PHPSpec, dan membutuhkan PHP 7.1+ dan Xdebug atau phpdbg untuk bekerja dengannya.
Peluncuran dan konfigurasi pertama
Pada awal pertama, kita melihat konfigurator interaktif dari kerangka kerja, yang membuat file khusus dengan pengaturan - infeksi.json.dist. Itu terlihat seperti ini:
{ "timeout": 10, "source": { "directories": [ "src" }, "logs": { "text": "infection.log", "perMutator": "per-mutator.md" }, "mutators": { "@default": true }
Timeout
- opsi yang nilainya harus sama dengan durasi maksimum satu tes. Dalam
source
kami menentukan direktori dari mana kami akan mengubah kode, Anda dapat mengatur pengecualian. Dalam
logs
ada opsi
text
, yang kami tetapkan untuk mengumpulkan statistik hanya pada tes yang salah, yang paling menarik bagi kami. Opsi
perMutator
memungkinkan Anda menyimpan mutator bekas. Baca lebih lanjut tentang ini di dokumentasi.
Contoh
final class Calculator { public function add(int $a, int $b): int { return $a + $b; } }
Katakanlah kita memiliki kelas di atas. Mari kita menulis tes di PHPUnit:
final class CalculatorTest extends TestCase { private $calculator; public function setUp(): void { $this->calculator = new Calculator(); } public function testAdd(int $a, int $b, int $expected): void { $this->assertEquals($expected, $this->calculator->add($a, $b)); } public function additionProvider(): array { return [ [0, 0, 0], [6, 4, 10], [-1, -2, -3], [-2, 2, 0] ]; } }
Tentu saja, tes ini perlu ditulis sebelum kami menerapkan metode
add()
. Saat mengeksekusi
./vendor/bin/phpunit
kita dapatkan:
PHPUnit 8.2.2 by Sebastian Bergmann and contributors. .... 4 / 4 (100%) Time: 39 ms, Memory: 4.00 MB OK (4 tests, 4 assertions)
Sekarang jalankan
./vendor/bin/infection
:
You are running Infection with Xdebug enabled. ____ ____ __ _ / _/___ / __/__ _____/ /_(_)___ ____ /
Menurut Infeksi, tes kami akurat. Dalam file
per-mutator.md, kita dapat melihat mutasi apa yang digunakan:
Mutator Plus adalah perubahan tanda sederhana dari plus ke minus, yang seharusnya mematahkan tes. Dan mutator
PublicVisibility
mengubah pengubah akses metode ini, yang juga harus memecahkan tes, dan dalam hal ini berfungsi.
Sekarang mari kita tambahkan metode yang lebih rumit.
public function findGreaterThan(array $numbers, int $threshold): array { return \array_values(\array_filter($numbers, static function (int $number) use ($threshold) { return $number > $threshold; })); } public function testFindGreaterThan(array $numbers, int $threshold, array $expected): void { $this->assertEquals($expected, $this->calculator->findGreaterThan($numbers, $threshold)); } public function findGreaterThanProvider(): array { return [ [[1, 2, 3], -1, [1, 2, 3]], [[-2, -3, -4], 0, []] ]; }
Setelah eksekusi, kita akan melihat hasil berikut:
You are running Infection with Xdebug enabled. ____ ____ __ _ / _/___ / __/__ _____/ /_(_)___ ____ /
Tes kami tidak benar. Pertama, periksa file infeksi.log:
Escaped mutants: ================ 1) /home/sarven/projects/infection-playground/infection-playground/src/Calculator.php:19 [M] UnwrapArrayValues --- Original +++ New @@ @@ */ public function findGreaterThan(array $numbers, int $threshold) : array { - return \array_values(\array_filter($numbers, static function (int $number) use($threshold) { + return \array_filter($numbers, static function (int $number) use($threshold) { return $number > $threshold; - })); + }); } 2) /home/sarven/projects/infection-playground/infection-playground/src/Calculator.php:20 [M] GreaterThan --- Original +++ New @@ @@ public function findGreaterThan(array $numbers, int $threshold) : array { return \array_values(\array_filter($numbers, static function (int $number) use($threshold) { - return $number > $threshold; + return $number >= $threshold; })); } Timed Out mutants: ================== Not Covered mutants: ====================
Masalah tidak tertangkap pertama adalah penggunaan fungsi
array_values
. Ini digunakan untuk mereset kunci, karena
array_filter
mengembalikan nilai dengan kunci dari array sebelumnya. Selain itu, dalam pengujian kami tidak ada kasus ketika diperlukan untuk menggunakan
array_values
, karena jika tidak array dengan nilai yang sama tetapi kunci yang berbeda dikembalikan.
Masalah kedua terkait dengan kasus perbatasan. Sebagai perbandingan, kami menggunakan tanda
>
, tetapi kami tidak menguji kasus batas apa pun, jadi mengganti dengan
>=
tidak merusak tes. Anda hanya perlu menambahkan satu tes:
public function findGreaterThanProvider(): array { return [ [[1, 2, 3], -1, [1, 2, 3]], [[-2, -3, -4], 0, []], [[4, 5, 6], 4, [5, 6]] ]; }
Dan sekarang Infeksi senang dengan segalanya:
You are running Infection with Xdebug enabled. ____ ____ __ _ / _/___ / __/__ _____/ /_(_)___ ____ /
Tambahkan metode
subtract
ke kelas
Calculator
, tetapi tanpa tes terpisah di PHPUnit:
public function subtract(int $a, int $b): int { return $a - $b; }
Dan setelah menjalankan Infeksi kita melihat:
You are running Infection with Xdebug enabled. ____ ____ __ _ / _/___ / __/__ _____/ /_(_)___ ____ /
Kali ini, alat tersebut mengembalikan dua mutasi yang tidak ditemukan.
Escaped mutants: ================ Timed Out mutants: ================== Not Covered mutants: ==================== 1) /home/sarven/projects/infection-playground/infection-playground/src/Calculator.php:24 [M] PublicVisibility --- Original +++ New @@ @@ return $number > $threshold; })); } - public function subtract(int $a, int $b) : int + protected function subtract(int $a, int $b) : int { return $a - $b; } 2) /home/sarven/projects/infection-playground/infection-playground/src/Calculator.php:26 [M] Minus --- Original +++ New @@ @@ public function subtract(int $a, int $b) : int { - return $a - $b; + return $a + $b; }
Metrik
Setelah setiap eksekusi, alat mengembalikan tiga metrik:
Metrics: Mutation Score Indicator (MSI): 47% Mutation Code Coverage: 67% Covered Code MSI: 70%
Mutation Score Indicator
- persentase mutasi yang terdeteksi oleh tes.
Metrik dihitung sebagai berikut:
TotalDefeatedMutants = KilledCount + TimedOutCount + ErrorCount; MSI = (TotalDefeatedMutants / TotalMutantsCount) * 100;
Mutation Code Coverage
- Proporsi kode yang tercakup oleh mutasi.
Metrik dihitung sebagai berikut:
TotalCoveredByTestsMutants = TotalMutantsCount - NotCoveredByTestsCount; CoveredRate = (TotalCoveredByTestsMutants / TotalMutantsCount) * 100;
Covered Code Mutation Score Indicator
- menentukan efektivitas tes hanya untuk kode yang dicakup oleh tes.
Metrik dihitung sebagai berikut:
TotalCoveredByTestsMutants = TotalMutantsCount - NotCoveredByTestsCount; TotalDefeatedMutants = KilledCount + TimedOutCount + ErrorCount; CoveredCodeMSI = (TotalDefeatedMutants / TotalCoveredByTestsMutants) * 100;
Gunakan dalam proyek yang lebih kompleks
Pada contoh di atas, hanya ada satu kelas, jadi kami menjalankan Infeksi tanpa parameter. Tetapi dalam pekerjaan sehari-hari pada proyek biasa akan berguna untuk menggunakan parameter
βfilter
, yang memungkinkan Anda untuk menentukan set file yang ingin kita terapkan mutasi.
./vendor/bin/infection --filter=Calculator.php
Positif palsu
Beberapa mutasi tidak mempengaruhi kinerja kode, dan Infeksi mengembalikan MSI di bawah 100%. Tetapi kita tidak selalu dapat melakukan sesuatu dengan ini, jadi kita harus menerima situasi seperti itu. Hal serupa ditunjukkan dalam contoh ini:
public function calcNumber(int $a): int { return $a / $this->getRatio(); } private function getRatio(): int { return 1; }
Tentu saja, di sini metode
getRatio
tidak masuk akal, dalam proyek normal, mungkin akan ada semacam perhitungan. Tetapi hasilnya bisa
1
. Pengembalian infeksi:
Escaped mutants: ================ 1) /home/sarven/projects/infection-playground/infection-playground/src/Calculator.php:26 [M] Division --- Original +++ New @@ @@ public function calcNumber(int $a) : int { - return $a / $this->getRatio(); + return $a * $this->getRatio(); } private function getRatio() : int
Seperti yang kita ketahui, mengalikan dan membaginya dengan 1 menghasilkan hasil yang sama, sama dengan angka aslinya. Jadi mutasi ini tidak boleh merusak tes, dan meskipun diseksi Infeksi mengenai keakuratan tes kami, semuanya beres.
Optimalisasi untuk proyek besar
Dalam kasus dengan proyek besar, Infeksi dapat memakan banyak waktu. Anda dapat mengoptimalkan eksekusi selama CI jika Anda hanya memproses file yang dimodifikasi. Untuk informasi lebih lanjut, lihat dokumentasi:
https://infection.imtqy.com/guide/how-to.htmlSelain itu, Anda dapat menjalankan tes pada kode yang dimodifikasi secara paralel. Namun, ini hanya mungkin jika semua tes independen. Yaitu, tes seperti itu seharusnya bagus. Untuk mengaktifkan opsi ini, gunakan
βthreads
:
./vendor/bin/infection --threads=4
Bagaimana cara kerjanya?
Kerangka kerja Infeksi menggunakan AST (Abstract Syntax Tree), yang merepresentasikan kode sebagai struktur data abstrak. Untuk ini, parser yang ditulis oleh salah satu pembuat PHP (
php-parser ) digunakan.
Pengoperasian alat yang disederhanakan dapat direpresentasikan sebagai berikut:
- Generasi AST berbasis kode.
- Penggunaan mutasi yang sesuai (daftar lengkapnya ada di sini ).
- Buat kode modifikasi berbasis AST.
- Jalankan tes terkait dengan kode yang diubah.
Misalnya, Anda dapat memeriksa mutator subtitusi minus plus:
<?php declare(strict_types=1); namespace Infection\Mutator\Arithmetic; use Infection\Mutator\Util\Mutator; use PhpParser\Node; use PhpParser\Node\Expr\Array_; final class Plus extends Mutator { public function mutate(Node $node) { return new Node\Expr\BinaryOp\Minus($node->left, $node->right, $node->getAttributes()); } protected function mutatesNode(Node $node): bool { if (!($node instanceof Node\Expr\BinaryOp\Plus)) { return false; } if ($node->left instanceof Array_ || $node->right instanceof Array_) { return false; } return true; } }
Metode
mutate()
membuat elemen baru, yang digantikan oleh nilai tambah. Kelas
Node
diambil dari paket php-parser, digunakan untuk operasi AST dan untuk memodifikasi kode PHP. Namun, perubahan ini tidak dapat diterapkan di mana pun, sehingga metode
mutatesNode()
berisi kondisi tambahan. Jika ada array di sebelah kiri plus atau ke kanan minus, maka perubahan tidak dapat diterima. Kondisi ini digunakan karena kode ini:
$tab = [0] + [1]; is correct, but the following one isn't correct. $tab = [0] - [1];
Ringkasan
Pengujian mutasi adalah alat yang sangat baik yang melengkapi proses CI dan memungkinkan Anda untuk mengevaluasi kualitas tes. Sorotan hijau dari tes tidak memberi kita keyakinan bahwa semuanya ditulis dengan baik. Anda dapat meningkatkan keakuratan pengujian menggunakan pengujian mutasional - atau pengujian pengujian - yang meningkatkan kepercayaan diri kami pada kinerja solusi. Tentu saja, tidak perlu mengusahakan 100% hasil metrik, karena ini tidak selalu mungkin. Anda perlu menganalisis log dan mengkonfigurasi tes yang sesuai.