Birim Testleri Mutantlara Karşı

Gif By TechPrimers

Üniversite yıllarında elime “Eliyahu M. Goldratt - Amaç kitabı geçmişti. Bir endüstri mühendisi yeni başladığı fabrika müdürlüğü görevinde ekibiyle birlikte üretim süreçlerini sorgulayarak bazı problemlere çözüm üretmeye çalışıyordu. Kalite kontrol süreçlerinin üretim aşamalarında erken safhalarda konumlandırılması ürün kalitesini arttıran en kritik çözümlerden birisiydi.

Yazılımcılar olarak bizler de henüz yazılım geliştirme aşamasında birim testlerinin sağladığı geri bildirimlerle yazılım kalitesini iyileştirmeye çalışıyoruz.

Birim Testleri

Öncelikle birim testlerini hatırlayalım. “Birim” (unit) olarak ifade edilen yazılımın en küçük parçalarının amaçlandığı gibi çalıştığını görebilmek için yazdığımız testlere birim testleri diyoruz.

Bu testler çalıştırıldığında test ettiği kod bloklarının ne kadarını kapsadığına da birim test kapsamı (unit test coverage) diyebiliriz. Bu kapsamı satır (line), komut (statement) ya da koşul(condition) kapsamı gibi farklı şekillerde de değerlendirebiliriz.

Basitçe satır kapsamı üzerinden örnek verirsek; birim testler çalıştığında 100 satırlık bir kodun 20 satırını test ediyorsa birim test kapsamı %20'dir.

(Birim testleri konusunda detaylı bilgi için bkz, The Art of Unit Testing)

Photo By Daniel Stori

%80 ve üzerinde birim test kapsamına sahip bir kod gördüğümüzde kaliteli olduğunu düşünürüz. Bu noktada bir mola verip kendimize şu soruları soralım:

  • Yazdığım birim testleri gerçekten doğru senaryoları mı test etmektedir ?
  • Peki takım arkadaşlarımın yazdığı birim testleri ?
  • Yazılan birim testlerinin kalitesinden nasıl emin olabilirim?
  • Bu testlerin de testlerini yazsak ? O zaman testin testinin de testini yazmak gerekecek. Testin testinin testinin de testi ? ( Burada dursak iyi olacak :) )
Photo By Ray Wenderlich

Birim testlerinin kalitesi düşük olduğunda yazılım kalitesi üzerinde nasıl bir illüzyon yaratır örnek bir projede birlikte görelim.

Buradan sizin için hazırladığım örnek projeyi indirerek başlayabiliriz. Proje içerisinde basit bir hesap makinası uygulaması ve uygulamayı test eden birim testlerini göreceksiniz. Projeyi indirip, derledikten sonra Visual Studio menüsünde “Test > Test Explorer“ üzerinden birim testlerini çalıştıralım ve tüm testlerin başarılı çalıştığını görelim.

Sonra Visual Studio menüsünden “Tools > Nuget Package Manager > Package Manager Console” açalım ve aşağıdaki komutu çalıştıralım.

dotnet test  
/p:CollectCoverage=true
/p:CoverletOutputFormat=opencover /p:CoverletOutput="Coverage/coverage.xml"
--test-adapter-path:. --logger:"xunit"

Sonuçları incelersek tüm testlerin başarılı çalıştığını ve yazılan testlerin %100 birim test kapsamı sağladığını görüyoruz.

Şimdi “CalculatorApp.UnitTests” projesi içindeki “CalculatorTests.cs” sınıfında yazılan testleri inceleyelim.

Test kodları içerisinde bazı problemler olduğunu görüyoruz. İlk iki testte hatalı ve eksik test doğrulamaları yapılmış. Sonuncu da ise düzgün yazılmamış ve hata alması muhtemel bir testin hata alması engellenmiş. “Acil bir kod aktarımı yapmam lazım şimdilik bu test geçsin sonra bakarız” mantığıyla yazılan ve yazıldığı gibi kalan testler…

LeBlanc’s Law: “Later equals Never.

Birim testlerinin yazımında yapılan hataların bizi nasıl yanlış yönlendirdiğini görmüş olduk. Peki bu gibi hataları nasıl tespit edebiliriz ?

Birim testi kavramının ortaya çıkmasından çok önce 1971 yılında Richard Lipton “Fault diagnosis of computer programs” isimli yayınında şöyle bir fikir ortaya attı;

Eğer testlerinizin uygulamayı doğru şekilde test edip etmediğini öğrenmek istiyorsanız, uygulamaya bir hata yerleştirin. Testlerin bu hatayı bulup bulamadığına bakın.

Basit ama çok zekice bir yaklaşım. Netflix’in üretim ortamında beklenmedik durumlar yaratarak (sunucuların kapatılması vb.) sistemin böyle anlarda nasıl davrandığı gözlemlemesini de buna örnek gösterebiliriz. (bkz, Kaos mühendisliği )

Richard Lipton’ın bu yaklaşımı mutasyon testinin temelini oluşturdu.

Mutasyon testi nedir?

Mutasyon testi, test senaryolarının kaynak koddaki hataları bulup bulamadığını kontrol etmek için kaynak kodun belirli bölümlerinin değiştirildiği / mutasyona uğradığı bir yazılım testi türüdür.

Örneğin birim testleriniz çalıştı ve herhangi bir hata almadan tamamlandı. Sonra test ettiğiniz kaynak kodda ufak bir değişiklik yapıyorsunuz. Değişiklik sonrası oluşan kod versiyonuna “Mutant” deniyor. Sonra aynı test setini mutant üzerinde çalıştırdığınızda herhangi bir test hata alırsa mutantı tespit etmiş oluyoruz.

Eğer test seti mutanta rağmen başarılı geçerse, mutant kurtulmuş oluyor. Bu durumda test setimizde eksiklik olduğunu görüyoruz. Test setinin mutantları bulabilme yetkinliğini gösteren mutasyon skoruna bakarak birim test kalitemizi ölçebiliyoruz. Mutasyon skoru %100'e ne kadar yakınsa o kadar iyi bir test setimiz var diyebiliriz.

Wolverine Photo By X-Men Origins: Wolverine
Mutasyon Skoru = 100 *D / (N - E)
D = Test setinin bulabildiği mutasyon sayısı
N = Toplam mutant sayısı
E = Benzer mutant sayısı
* Ek bilgi: Mutasyon testi yapan ürünlerde benzer özellikler gösteren mutantlar üretilebiliyor. Performans amacıyla mükerrer mutantlar toplam mutant sayısından düşülerek hesaplama yapılıyor.

Mutasyon testi gelişim süreci

Richard Lipton’ın yaktığı ışık sonrası mutasyon test kavramı yaklaşık 30 sene akademik çalışmalarla devam etti. İlk olarak 2000'li yıllarda “Jester” adında bir ürün ortaya çıktı.

Sometimes Jester tells me my tests are airtight, but sometimes the changes it finds come as a bolt out of the blue. Highly recommended.”

“Bazen Jester bana testlerimin hava geçirmez olduğunu söylüyor, ancak bazen bulduğu değişiklikler hiç beklemediğiniz sürpriz sonuçlar ortaya çıkarıyor. Şiddetle tavsiye edilir”

Kent Beck

Kent Beck’in bu yorumu sonrasında ne oldu dersiniz? Kimse kullanmadı :)

Çünkü en büyük problem yavaşlıktı. Kod satır sayısı fazla olmayan projelerde bile mutasyon testi saatleri bulabiliyordu. Başlıca sebepleri:

  • Yapılan her kod değişikliği sonrası tekrar tekrar derlemelerin yapılması
  • Her derlemede binlerce testin tekrar tekrar çalıştırılması
  • Benzer özelliklere sahip mutantların oluşturulması

Zamanla hem teknolojinin gelişmesi hem de programlama dillerinin özelliklerinin daha iyi kullanılmasıyla önemli gelişmeler sağlandı.

  • Derleme sayısını minimuma indirerek “byte code manipulation” vb. tekniklerle süreden kazanılması
  • Mutasyon testlerinin paralel çalıştırılması
  • Mutasyon testlerinde öncelik belirlenmesi, maliyeti düşük testlerin önce çalıştırılması
  • Herhangi bir kod satırını test etmeyen etkisiz veya benzer özellikteki mutantların devre dışı bırakılması

vb. çözümlerin uygulanmasıyla artık daha hızlı bir şekilde mutasyon testi yapabilmekteyiz.

Stryker.NET ile mutasyon testi

Programlama dilleri özelinde açık kaynak birçok mutasyon test ürünü bulunuyor. (bkz, Top 21 Mutation Testing Open Source Projects)

Stryker Mutator .net desteği olan bir ürün. Hazırsanız mutasyon testinin nasıl çalıştığını örnek projemiz üzerinde birlikte görelim.

Öncelikle kurulum için örnek projemizin “.\mutation-testing” dizininde aşağıdaki komutu çalıştıralım.

dotnet tool install dotnet-stryker

Kurulumun başarılı olduğunu gördükten sonra birim test projesinin bulunduğu .\Calculator.UnitTests dizininde aşağıdaki komutu çalıştırarak mutasyon testimizi başlatalım.

dotnet stryker

Sonuçlara baktığımızda mutasyon test skorumuz %25 çıktı. Komut çıktısında dikkat edersek analiz etmemiz için “report.html” uzantılı bir rapor da üretildiğini görüyoruz.

Raporda mutantların Killedve Survived olarak gruplandığını görüyoruz. Eğer testlerimiz mutantların hakkından gelmişse “Killed” bölümündeki sayı, tersi durumda da “Survived” sayısı artıyor.

Yeşil renklendirilmiş yerler test setimizin başarılı olduğu bölümleri gösteriyor. Kırmızı bölümlerde ise mutasyona uğrayan kod değişikliklerini testlerimiz yakalayamamış ve başarısız olmuşuz.

Kırmızı bölümlerin üzerine geldiğimizde hangi tipte bir mutasyon yapıldığını görebiliyoruz. Örneğimizde “Aritmetik Operator” değişikliği yapılmış.(Farklı mutasyon örnekleri için bkz, Stryker Mutators)

Toplama işlemindeki “+” değeri ”-” olarak, çıkarma işlemindeki “-” değeri ”+” olarak değiştirilmiş fakat testlerimiz bu değişiklikleri yakalayamamış. Hatırlayalım testlerimizde eksik ve hatalı doğrulamalar bulunuyordu.

Mutant savaşları

Resmi olarak birim testlerimize savaş ilan edildi :) Testlerimizdeki eksik ve hatalı doğrulama problemlerini aşağıdaki gibi gidererek karşılık verelim.

Add metoduna yazılan testte hatalı doğrulama satırını;
result.Should().Be(8); olarak değiştirelim.
Subtract metoduna ait testte eksik olan;
result.Should().Be(2); satırını testin son satırı olarak ekleyelim.

Mutasyon testini tekrar çalıştırdığımızda yeni oluşan raporda mutasyon skorunun %75'e geldiğini görüyoruz. Mutantların büyük bölümünün hakkından gelmişiz. Son mutantı da size bırakıyorum :)

Mutasyon testi kazanımları

  • Birim test kapsamı, test kodlarının kaynak koddaki hangi satırları test ettiği bilgisine odaklanır. Öte yandan, mutasyon testi de testlerimizin işe yaradığından emin olmanın bir yoludur.
  • Mutasyon testi bize birim testlerimizin gerçekte neyi test ettiğine dair çok daha net bir görünüm verir.
  • Mutasyon testleri zayıf birim test senaryolarını tespit eder.
  • Kaynak kodumuzun kalitesini kontrol etmemize yardımcı olur.

Hedeflerinizi belirleyin

İş hayatında birim testlerini yazmak örneğimizdeki kadar kolay olmuyor. Yüzlerce satıra sahip metotları, karmaşık kod bloklarının bulunduğu projeleri test yazılabilir hale getirmek ciddi bir maliyet. Projelerdeki zaman baskıları, teknik bilgi eksiklikleri vb. faktörlerin etkisiyle kalite genelde ikinci planda kalıyor.

Bunun yanında mutasyon testi aşamasına gelebilmek de kolay değil. Mevcut alışkanlıkları bir kenara bırakıp test odaklı yazılım geliştirme pratiklerine direnç de fazla oluyor. Burada sevdiğim bir sözü paylaşmak isterim:

My dentist said to me, “If you don’t floss, then you might as well not bother brushing.

I have ammended this to “If you don’t write Unit Tests, then you might as well not bother coding.”

Dişçim bana, “Diş ipi kullanmazsan, dişlerini fırçalamaya da zahmet etmeyebilirsin” dedi. Bunu “Birim Testleri yazmazsanız, kodlama zahmetine girmeyebilirsiniz” şeklinde değiştirdim.

Scott Hanselman Law Of Unit Testing

Mutasyon testinin yapısında oyunlaştırma olması sebebiyle iş biraz daha eğlenceli hale geliyor. Mutantlarla hiç bitmeyecek bir savaşın içinde buluyorsunuz kendinizi. Denemenizi tavsiye ediyorum. Testlerinizdeki eksiklikleri gördüğünüzde şaşıracaksınız eminim.

Geliştirdiğiniz uygulamalara birim test kapsamı, teknik borçlanma vb. kalite metrikleri için somut hedefler koyup mutasyon testleriyle de desteklediğinizde yazılım kalitenizin arttığını göreceksiniz.

Eğer birim testlerinize çok güveniyorsanız mutantlar sizi bekliyor :)