Pengujian mutasi: pengujian pengujian


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 { /** * @var Calculator */ private $calculator; public function setUp(): void { $this->calculator = new Calculator(); } /** * @dataProvider additionProvider */ 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. ____ ____ __ _ / _/___ / __/__ _____/ /_(_)___ ____ / // __ \/ /_/ _ \/ ___/ __/ / __ \/ __ \ _/ // / / / __/ __/ /__/ /_/ / /_/ / / / / /___/_/ /_/_/ \___/\___/\__/_/\____/_/ /_/ Running initial test suite... PHPUnit version: 8.2.2 9 [============================] 1 sec Generate mutants... Processing source code files: 1/1Creating mutated files and processes: 0/2 Creating mutated files and processes: 2/2 .: killed, M: escaped, S: uncovered, E: fatal error, T: timed out .. (2 / 2) 2 mutations were generated: 2 mutants were killed 0 mutants were not covered by tests 0 covered mutants were not detected 0 errors were encountered 0 time outs were encountered Metrics: Mutation Score Indicator (MSI): 100% Mutation Code Coverage: 100% Covered Code MSI: 100% Please note that some mutants will inevitably be harmless (ie false positives). Time: 1s. Memory: 10.00MB 

Menurut Infeksi, tes kami akurat. Dalam file per-mutator.md, kita dapat melihat mutasi apa yang digunakan:

 # Effects per Mutator | Mutator | Mutations | Killed | Escaped | Errors | Timed Out | MSI | Covered MSI | | ------- | --------- | ------ | ------- |------- | --------- | --- | ----------- | | Plus | 1 | 1 | 0 | 0 | 0 | 100| 100| | PublicVisibility | 1 | 1 | 0 | 0 | 0 | 100| 100| 

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.

 /** * @param int[] $numbers */ public function findGreaterThan(array $numbers, int $threshold): array { return \array_values(\array_filter($numbers, static function (int $number) use ($threshold) { return $number > $threshold; })); } /** * @dataProvider findGreaterThanProvider */ 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. ____ ____ __ _ / _/___ / __/__ _____/ /_(_)___ ____ / // __ \/ /_/ _ \/ ___/ __/ / __ \/ __ \ _/ // / / / __/ __/ /__/ /_/ / /_/ / / / / /___/_/ /_/_/ \___/\___/\__/_/\____/_/ /_/ Running initial test suite... PHPUnit version: 8.2.2 11 [============================] < 1 sec Generate mutants... Processing source code files: 1/1Creating mutated files and processes: 0/7 Creating mutated files and processes: 7/7 .: killed, M: escaped, S: uncovered, E: fatal error, T: timed out ..M..M. (7 / 7) 7 mutations were generated: 5 mutants were killed 0 mutants were not covered by tests 2 covered mutants were not detected 0 errors were encountered 0 time outs were encountered Metrics: Mutation Score Indicator (MSI): 71% Mutation Code Coverage: 100% Covered Code MSI: 71% Please note that some mutants will inevitably be harmless (ie false positives). Time: 1s. Memory: 10.00MB 

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. ____ ____ __ _ / _/___ / __/__ _____/ /_(_)___ ____ / // __ \/ /_/ _ \/ ___/ __/ / __ \/ __ \ _/ // / / / __/ __/ /__/ /_/ / /_/ / / / / /___/_/ /_/_/ \___/\___/\__/_/\____/_/ /_/ Running initial test suite... PHPUnit version: 8.2.2 12 [============================] < 1 sec Generate mutants... Processing source code files: 1/1Creating mutated files and processes: 0/7 Creating mutated files and processes: 7/7 .: killed, M: escaped, S: uncovered, E: fatal error, T: timed out ....... (7 / 7) 7 mutations were generated: 7 mutants were killed 0 mutants were not covered by tests 0 covered mutants were not detected 0 errors were encountered 0 time outs were encountered Metrics: Mutation Score Indicator (MSI): 100% Mutation Code Coverage: 100% Covered Code MSI: 100% Please note that some mutants will inevitably be harmless (ie false positives). Time: 1s. Memory: 10.00MB 

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. ____ ____ __ _ / _/___ / __/__ _____/ /_(_)___ ____ / // __ \/ /_/ _ \/ ___/ __/ / __ \/ __ \ _/ // / / / __/ __/ /__/ /_/ / /_/ / / / / /___/_/ /_/_/ \___/\___/\__/_/\____/_/ /_/ Running initial test suite... PHPUnit version: 8.2.2 11 [============================] < 1 sec Generate mutants... Processing source code files: 1/1Creating mutated files and processes: 0/9 Creating mutated files and processes: 9/9 .: killed, M: escaped, S: uncovered, E: fatal error, T: timed out .......SS (9 / 9) 9 mutations were generated: 7 mutants were killed 2 mutants were not covered by tests 0 covered mutants were not detected 0 errors were encountered 0 time outs were encountered Metrics: Mutation Score Indicator (MSI): 77% Mutation Code Coverage: 77% Covered Code MSI: 100% Please note that some mutants will inevitably be harmless (ie false positives). Time: 1s. Memory: 10.00MB 

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.html

Selain 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:

  1. Generasi AST berbasis kode.
  2. Penggunaan mutasi yang sesuai (daftar lengkapnya ada di sini ).
  3. Buat kode modifikasi berbasis AST.
  4. 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_; /** * @internal */ final class Plus extends Mutator { /** * Replaces "+" with "-" * @param Node&Node\Expr\BinaryOp\Plus $node * @return Node\Expr\BinaryOp\Minus */ 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.

Source: https://habr.com/ru/post/id457888/


All Articles