plazma - amatör bilgisayar kültürü

Yazılım Doğrulamaya Giriş

Bilgem 'Nightlord' Çakır

Bu yazımızda son yıllarda giderek önemi artan yazılım doğrulama konusuna giriş yapacağız. Yazılım testi konusundaki temel problemlerden ve tekniklerden bahsedeceğiz.

1. Yazılım Doğrulama nedir.

Bu konuda pek çok değişik tanım bulmak mümkün. Bazı tanımlar işin teknik tarafına yoğunlaşırken, kimi tanımlar ise konunun yönetimsel tarafına değiniyor. İki örnek tanıma bakalım.

Yazılım Doğrulama bir programın spesifikasyonlarında anlatıldığı şekilde davranıp davranmadığını sınama aktivitesidir.

Yazılım Doğrulama, bir yazılım projesinin zamanında ve bütçe dahilinde, ölçülebilir olarak hedeflenen spesifikasyonlara doğru ilerlemesini sağlayan aktiviteler bütünüdür.

Her iki tanımda da ortak olan bazı öğeler olduğunu görebilirsiniz. Dikkatinizi çekmesi gereken belki de en önemli nokta test edilen programın bir spesifikasyonu olması gerektiği. Yani doğrulamayı yapan kişilerin programı neye göre doğruladıklarını bilmeleri gerekir. Programın ne yapmayı vaad ettiğini bilmeden, vaad ettiği şeyi yapıp yapmadığını doğrulamak mümkün değildir. Spesifikasyonlara bundan sonra kısaca Spek diyeceğiz.

2. Doğrulama Türleri

Yazılım doğrulama çabası farklı türlerde testlerin yapılmasıyla yürütülür. Değişik amaçları olan farklı türlerde testler programınızın farklı yönlerden doğrulanmasına yardımcı olur ve kalitesinin artmasını sağlar

2.1. Kapalı Kutu Testleri

Eğer yazdığınız yazılımı kapalı bir kutu olarak düşünür ve sadece Spekte belirtilen davranışları test etmeye çalışırsanız kapalı kutu testleri yapıyorsunuz demektir. Genelde kapalı kutu testleri, sadece test edilen yazılımın arayüzünü kullanarak sisteme çeşitli girdiler verip, sistemin beklenen şekilde davranıp davranmadığına bakarlar. Bu arayüz bir GUI programında penceredeki düğmeler, text alanları vs olabilir. Bir konsol programında verilen opsiyonlar olabilir. Bir kütüphane için API olabilir. Eğer programınız harddiskten bir data dosyası kullanıyorsa o dosya olabilir. Veya Network haberleşmesi yapan bir programa gelen network paketleri de "arayüz"deki girdiler olabilir. Kapalı kutu testi yaparken hep böyle girdiler hazırlayıp sisteme verir, ve sistemin verdiği cevaba bakarız. Bu girdileri hazırlarken nelere dikkat etmemiz gerektiğine birazdan değineceğiz.

2.2. Şeffaf kutu testleri

Çoğu zaman sadece kapalı kutu testleri yapmak yeterli olmayacaktır. Özellikle programınız dar bir arayüze sahip olmasına karşın çok karmaşık işlemler yapıyorsa (ki çoğu zaman programlarımızı tasarlarken böyle olmasını hedefleriz) mutlaka şeffaf kutu testleri de yapmak gerekir. Bu testleri hazırlarken sisteminizin içinde yer alan algoritmaları saklanan bilgileri vs dikkate alarak hareket edersiniz. Bunun nasıl olduğunu da birazdan anlatacağız.

2.3. Zorlayıcı Testler

Özellikle projeniz bir miktar ilerledikten sonra, yazdığınız programın ağır yükler altında nasıl davrandığını görmek isteyebilirsiniz. Örneğin network üzerinden çalışan bir sunucunun, çok fazla sayıda istemci olduğunda nasıl davrandığını test etmek, veya bir data dosyasını inceleyen programınıza devasa büyüklükte dosyalar vermek, bu sınıfa giren testlerdir. Bu yazımızda bu testlere değinmeyeceğiz.

2.4. Performans testleri

Çoğu zaman speklere programınızın performansı ile ilgili vaatler konabilir. Örneğin programın yakalamayı hedeflediği frame rate veya kullanıcının düğmeye basması ile ekrana menünün çıkması arasındaki maksimum süre gibi hedefler olabilir. Bazı sistemlerde bunlar hayati önem de taşıyabilir. Bu yüzden programın performans hedeflerini tutturabildiğini doğrulamak için çeşitli testler yapılabilir. Bu testler genellikle sistemde bulunan çeşitli zamanlayıcıları kullanarak, iki olay arasındaki zaman farkını ölçüp istenen değerin altında kalıp kalmadığını kontrol ederler.

2.5. Hataların geri gelmesini önleme testleri

Çok önemli bir test grubu da budur. Genelde programın geliştirme aşamalarında pekçok hata ortaya çıkar. Bulunan hatalar geliştiriciler tarafından giderilmeye çalışılır. Fakat çok sık yaşanan bir problem, bazı hataların düzeltilmesine rağmen tekrar tekrar su yüzüne çıkmasıdır.

Bunun sebebi genellikle, bir geliştiricinin bir hatayı düzeltirken, daha önce yapılan bir düzeltmeyi bozabilmesi, ve eski bugların yeniden ortaya çıkmasına sebep olmasıdır.

Bu yüzden, genellikle düzeltilen her hata için, o düzeltmeyi doğrulayan bir test hazırlanır. Ardından bu test, böyle diğer bütün testlerle beraber grup olarak periyodik aralıklarla çalıştırılır. Genellikle bir grup testin her gün çalıştırılması otomatize edilir. Böylece eğer bir geliştirici kodda yanlışlıkla eski bir hatayı geri getirecek bir değişiklik yaparsa, bu durum hemen bir gün içinde otomatik testlerce yakalanır. Böylece projenin hep ileri doğru gitmesi sağlanır.

2.6. Veri Bozma Testleri

Son olarak değineceğimiz test türü de veri bozma testleri. Bu testlerle, genelde bir programın girdi olarak aldığı veriler rastgele veya belli kurallara bağlı olarak bilinçli olarak bozulur ve sisteme verilir. Sistemin bu koşullarda, bozuk veriyi zarif bir şekilde (yani göçmeden) reddetmesi veya bazı küçük hataları düzeltebilmesi isteniyor olabilir. Bu konuda beklentiler her ne ise (bu beklentiler de spekte olmalı), bu beklentilerin gerçekleşip gerçekleşmediği de bu testlerle sınanabilir.

3. Yazılım Doğrulama Birimleri: Test Birimleri

Buraya kadar anlattığımız bütün test türlerinde genellikle testler küçük veri gruplarından oluşur. Bir seferde programa verilecek veriler ve programın vermesini beklediğimiz cevap bir "Test Birimi"ni oluşturur.

Örneğin elinizde toplama yapan bir program var. Bu programa iki sayı veriyorsunuz ve o da toplamını döndürüyor. Bu programa 3 ve 5 verip cevap olarak 8 beklemek bir test birimidir.

İyi test birimleri tasarlamak çok önemli bir yetidir. Her programın veya kütüphanenin test birim tasarımı farklı olacaktır. Fakat test birimleri tasarlarken dikkat edilecek bazı ortak noktalar var. Bunlara değinelim.

4. Kapalı kutu test tasarımı

Kapalı kutu test tasarımı yaparken bildiğiniz (veya bildiğinizi varsaydığınız) tek şey test ettiğiniz yazılımın spekidir. Spekin parçası olarak programla nasıl etkileşileceği (yani arayüz) açıklanmıştır. Mesela daha önce değindiğimiz toplama örneğini ele alalım.

Toplama yazılımı: Bu yazılım iki adet iki basamaklı pozitif sayıyı girdi olarak alır, bu sayıların toplamını çıktı olarak döndürür. Eğer hatalı bir girdi verilirse sonuç -1 olarak döndürülür.

Bu örnekte bahsettiğimiz yazılım bir gui programı da olabilir, bir konsol programı da, ya da bir APInin parçası olan bir fonksiyon da olabilir. Biz burada bunu önemsemiyoruz. Yani girdileri, GUI veya konsola mı veriyoruz, yoksa bir fonksiyona arguman olarak mı geçiriyoruz, buna şimdi dikkat etmeyeceğiz. Biz daha çok test birimini belirlemeye çalışacağız. Yani bu yazılımı hangi sayı çiftleri ile test edeceğimizi tartışacağız.

4.1. Sınır Koşulları

Bir programa verilen olası girdi değerlerinin tümünü deneyebilseydik programla illgili kendimizi çok güvende hissedebilirdik. Fakat bu genellikle imkansızdır. Bu yüzden bir Test Birimi tasarlarken mutlaka girdilerin alabileceği hangi değerlerin kritik olduğuna ve olası hataları ortaya çıkarabileceğine bakarız.

Genel olarak programlar kabul ettikleri girdiler ile ilgili bazı sınırlar belirtirler. Bu sınırların dışında kalan değerlerin kabul edilmediğini veya bir sınırın üstünde ve altında değerler için farklı işlemler yapıldığını belirtebilirler.

Genellikle pekçok hata bu sınırlar ve onların hemen yakınında olur. Mesela örnek programımızda verilen sayıların iki basamaklı pozitif sayılar olması bekleniyor. Programın içinde sayının üst sınırının kontrol edildiğini düşünelim. Bu kontrolu yapan kod şöyle birşey olabilir.

if ( Verilen_Sayi < 99 ) 
{
    ...
      

veya belkide şöyledir:

if ( Verilen_Sayi >= 100 )
{
     ...
      

ya da böyle:

if ( Verilen_Sayi < 100 )
{
    ...
      

Dikkat ederseniz buradaki karşılaştırma koşulu bir olası hata noktası. Verdiğimiz ilk iki kod hatalı. İlki geçerli bir girdi olan 99 değerini reddedecek. İkincisi ise geçersiz bir girdi olan 100 değerini kabul edecek.

Böyle problemlere çok sık rastlandığı için genelde sınır değerleri yakınında ve üzerinde değerler içeren test birimleri hazırlamak mantıklıdır. Örneğin;

  • -1, 5

  • 0, 5

  • 1, 5

  • 98, 5

  • 99, 5

  • 100, 5

  • 12, -1

  • 12, 0

  • 12, 1

  • 12, 98

  • 12, 99

  • 12, 100

Bu 12 test birimi ile sınır koşullarını her iki girdi için de test etmiş oluyoruz. Eğer bütün olasılıkları test edecek olsaydık 100 x 100 toplam 10000 test birimi hazırlamak gerekirdi. Burada sadece 12 test birimi ile 10000 birimlik testimize çok yakın bir güven elde edebiliriz (Bu güveni nasıl nümerik olarak ölçebiliriz birazdan göreceğiz)

4.2. Denklik Sınıfları

Bir programa giren girdiler, eğer sürekli bir kümeden gelmiyorsa, sınırlardan bahsetmek mümkün olmayabilir. Örneğin haftanın günlerinden biri girdilerden birini oluşturuyor ise, ve program hafta sonu için farklı hafta içi için farklı davranacağını söylüyorsa sınırlar nedir. Cuma olduğunu düşünebilirsiniz. Belki... Ama bu tip durumlar (ve sürekli kümeler için de) sınır koşullarına bakmak dışında bir perspektif daha vardır. Girdinin alabileceği değerler kümesini "denklik sınıfları"na bölmek.

Denklik sınıfları, adından da tahmin edebileceğiniz gibi bir girdinin alabileceği bütün değerleri speklere göre sınıflara ayırır. Aynı sınıfa ait üyelerin ayni şekilde işlem göreceği ve bir denklik sınıfından sadece bir (veya birkaç) değerin testte kullanılması öngörülür. Bu şekilde tıpkı sınır koşullarını kullandığımız zamanki gibi test birimi sayısını azaltabiliriz.

5. Kod Kapsama ve Şeffaf kutu testleri

Az önce programa ve testlerimizin programın hatalarını ne kadar yakaladığına dair güvenimizi nasıl ölçebileceğimizi sorgulamıştık. Bu kaygıyı birebir cevaplamamakla beraber, bazı sayısal metrikler mevcuttur ve sayısal metrikler her zaman "içimden bir ses böyle söylüyor"dan daha iyidir.

Fakat ne zaman ki kod kapsamadan bahsediyoruz, o zaman kapalı kutudan bahsetmiyoruz demektir. Çünkü birazdan göreceğiniz gibi, kod kapsama tekniklerinden faydalanmak, yazılan kodun içini bilmeyi ve bu bilgiden faydalanmayı gerektirir. Bu bilgiyi kullanarak yapılan testlere de "Şeffaf Kutu Testleri" denir.

Aşağıdaki fonksiyona bir bakın:

int Topla( int Sayi1, int Sayi2)
{
    if ((Sayi1>=0)&&(Sayi1<100)){
        if((Sayi2>=0)&&(Sayi2<100)){
            return(Sayi1+Sayi2);
        }else{
            //Sayi 2 istenilen aralikta degil
            return -1;
        }
    }else{
        // Sayi 1 istenilen aralikta degil
        return -1
    }
}
    

Bu fonksiyonu test ederken ne zaman emin oluruz. İşte kod kapsama kavramı burada devreye girer. Kod kapsama, yazılan bir kodun içindeki değişik birimlerden hangilerinin testler esnasında sınandığını ve ne kadarının sınanmadan geçtiğini ifade eder. Değişik kod kapsama türleri vardır. Şimdi bunlardan bir kaçına değinelim.

5.1. Kod satırlarını veya kod dallarını kapsamak

Bu örneğimizi bir adet test birimi ile test ettiğimiz zaman hangi satırların çalıştığına bakabiliriz. Koşullu ifadelerden (yani if-else ) dolayı bir test birimi bu fonksiyondaki her satıra uğramayacaktır.

Bir grup test birimini çalıştırdıktan sonra fonksiyonda uğranan satırların sayısını daha da artırmış oluruz. Böylece iyi seçilmiş test birimleri ile her hangi bir programın sahip olduğu kod satırlarının büyük bölümünün "kapsanmasını" sağlayabiliriz. Ve diyebiliriz ki bu programımızı testlerimizle %85 satır kapsaması sağlayarak test ettik. Bu ölçülebilir bir metriktir. Örneğin koda eklemeler yaptıkça kapsama yüzdeniz azalacaktır. Bir proje ekibi kapsama yüzdesi örneğin %70'in altına düşen bir yazılımı çıkarmayı reddedebilir. Çünkü kodun %30unun test edilmediği ve orada kritik buglar olabileceğini düşünebilirler.

Bazen de koddaki satırların kapsamasını incelemek yerine koddaki dalların (blokların) kapsamasını inceleyebiliriz. Bunu takip etmek kimi projelerde daha mantıklı olabilir. Fakat temelde aynı mantıktır.

Son olarak Kapsama değerlerini ölçen programlar vardır. Yani kapsama değerlerini siz elle ölçmezsiniz. Bunun için favori derleyicinizin kod kapsama (code coverage) özelliklerini araştırın

5.2. Koşul ifadelerini kapsamak

Daha önce en çok hata yapılan yerlerin koşullar olduğundan bahsetmiştik. Eğer karmaşık koşul ifadeleri yazılırsa, dikkat etmemiz gereken bir nokta daha var. Şu örneğe bir bakın. Eğer elimizde şöyle bir if satırı olsaydı:

if ( ( Sayi1 >= 0 ) && ( Sayi2 < 99 ) )
      

Bu if satırından dolayı kod iki farklı yere atlayabilir. Ya bir alt satıra ya da else bölümüne. Diyelim ki Sayı1 ve Sayı2 için sırasıyla -1,99 değerlerini verdik. Bu durumda aradaki && işleminin yarattığı bir problem var. Bu operator kendisinden önceki ilk koşula (Sayı1 >= 0) bakıp onun yanlış olduğunu gördüğü anda else bölümüne atlar. Yani ikinci koşulu test etmez. Bu yüzden biz ikinci koşulda yaptığımız hatayı farketmeyebiliriz.

Bu yüzden kimi projeler metrik olarak "Koşul Kapsaması"nı da kullanırlar. Test birimlerinin hangi koşul ifadelerine uğradığını takip ederler. Kod satırı kapsamadan farklı olarak genelde koşul kapsama metrikleri kullanıldıkları yerlerde %100 koşul kapsaması aranır.

5.3. Durum Motorlarını kapsamak

Bir diğer şeffaf kutu yaklaşımı da durum motorlarını test ederken karşımıza çıkar. Bir yazılım eğer çeşitli durumlara ve durumlar arası tanımlı geçişlere sahip bir durum motoru olarak tasarlanmış ise, bu durumda test birimlerimizin hangi durumları ve hangi geçişleri kapsadığından bahsedebiliriz. Durum motorlarını test ederken ayrıca komutların gonderiliş sıraları da karıştırılarak durum motorunun tepkisi test edilir. Bu konuyu bu yazıda daha detaylı incelemeyeceğiz

5.4. Hata Enjeksiyonu

Diyelim ki kapalı kutu testleriyle başladınız. Speklere baktınız ve görebildiğiniz sınır koşulları ve denklik sınıflarını kullanarak 50 tane test birimi hazırladınız. Sonra bu testlerin sağladığı kod kapsamasını ölçtünüz ve %65 çıktı. Ardından şeffaf kutu yaklaşımına geçtiniz. Uğranmayan kod dallarını tespit edip onların bir kısmına uğranmasını sağlayacak şekilde 10 tane daha test birimi eklediniz. Tekrar kapsamayı ölçtünüz ve %90 çıktı. Biraz inceleyince neden bazı kod dallarına hiç uğramadığınızı gördünüz. Mesela şöyle kodlar var.

cSoldier oSoldier = new Soldier( kBlue );
if ( oSoldier == NULL ){
    std::cout << "bellek bitti" 
              << std::endl;
    exit 1;
}
      

veya;

XYZLibraryError_t oError = XYZLibraryInit();
if ( oError != XYZ_NO_ERROR ){
    std::cout << "XYZ kutuphanesini baslatamadim" 
              << std::endl;
    exit 1;
}
      

bu tip if'lerin içine giremiyorsunuz, çünkü bazı ekstra hatalar olması gerekiyor. Sizin testlerinizde doğal olarak makinenizin belleği normal şartlarda bitmiyor ve XYZ kütüphanesi hep başarıyla başlayabiliyor.

Bu noktada mevcut kapsama değerleriyle yetinip daha fazla test birimi üretmeyebilirsiniz. Bu çoğu zaman mantıklı bir karardır. Bazı projelerde bundan fazlası gerekebilir. Mesela Aya İniş Modülünün yazılımını yazıyorsanız, ve oradaki astronota "pardon ya bellek bitti ben çıkıyorum" diyemeyeceğiniz için, bu tip hata koşullarının da test edilmesi gerekebilir. İşte bu test yaklaşımına da "Hata Enjeksiyonu" deniyor.

Hata enjeksiyonu yapmak değişik şekillerde mümkün olabilir. Kütüphaneler yerine aynı arayüze sahip ve sizin istediğiniz hataları döndüren taklit fonksiyonlar (bunlara stub denir) yazabilirsiniz. Ya da test ettiğiniz sistemde bir takım harici araçlarla hata koşullarını yaratabilirsiniz.

6. Sonuç

Yazılım doğrulama alanı yazılım geliştirmeye göre daha yeni ve fazla çözülmemiş bir alandır. Fakat yine de yazılım geliştirme projelerinde kaliteyi ve ilerlemeyi ölçen önemli bir metrik sağlar. Yazılım doğrulamanın alt alanlarından kısaca bahsettiğimiz ve Test Birimi tasarlamanın temellerine giriş yaptığımız bu yazı umarız ileriki projelerinizde faydalı olur.

nightlord (at) nightnetwork (nokta) org

7. Yararlanilan Kaynaklar:

  • Testing Computer Software 2nd Ed.; Kaner, Falk, Nguyen

  • How to Break Software: A Practical Guide to Testing; Whittaker

  • Ship it! A Practical Guide to Successful Software Projects; Richardson, Gwaltney

plazma - 2008